Delphi XE8 et JSON


Mise à jour le 26/07/2017 pour le support des CR/LF

Depuis pas mal de versions maintenant, Delphi nous propose non pas une mais deux gestions de JSON. La première utilise l'unité Data.DBXJSONReflect pour transformer un objet Delphi en chaîne JSON et vice versa. L'autre, dont le code est relativement proche se trouve dans REST.JSONReflect, le code JSON généré n'est pas le même car il est débarrassé au maximum des notions objets de Delphi afin de faciliter la transmission d'un objet métier Delphi vers un code JavaScript d'un client DataSnap web (par exemple).

JSON Reflect

Si vous recherchez sur google delphi docwiki XE8 JSONReflect - pensez à préfixer vos recherches de "delphi docwiki" pour accéder à l'aide en ligne Embarcadero - vous pourrez trouver le lien vers REST.JsonReflect.TJSONMarshal qui, à ce jour nous indique que :
Embarcadero Technologies ne possède pas d'informations supplémentaires pour le moment. Veuillez nous aider à documenter cette rubrique en utilisant la page Discussion !
ouille !

Mais en persévérant, vous trouverez sous delphi docwiki JSON Marshal la page expliquant la sérialisation des objets avec JSON

La page n'est pas très explicite mais elle donne tout de même quelques bases, voici pour allez vite comme sérialiser un objet avec JSONMarshal

var
  o: TMonObjet;
  m: TJSONMarshal;
  s: string;
begin
  o := TMonObjet.Create; // création de notre objet
  m := TJSONMarshal.Create(TJSONConverter.Create); // création du Marshal
  s := m.Marshal(o).toJSON(); // conversion de notre objet en chaîne JSON
  m.Free; // plus besoin du marshal
  o.Free; // plus besoin de l'objet
end;

mais voyons ce que ça donne avec un exemple simple:

type
  TSimpleObject = class
    Name : string;
    Date : TDateTime;
    Color: TColor;
    Alive: Boolean;
    Lines: TArray<string>;
    Bonus: string;
  end;
var
  o: TSimpleObject;
  m: TJSONMarshal;
  s: string;
begin
  o := TSimpleObject.Create;
  o.Name := 'Paul';
  o.Date := Now();
  o.Color := clRed;
  o.Alive := True;
  o.Lines := ['One', 'Two', 'Three'];
  o.Bonus := 'Cadeau';  

  m := TJSONMarshal.Create(TJSONConverter.Create); // création du Marshal
  s := m.Marshal(o).toJSON(); // conversion de notre objet en chaîne JSON
  m.Free; // plus besoin du marshal
  o.Free; // plus besoin de l'objet
end;

à l'execution de ce code, nous obtenons dans la variable s la valeur suivante :

{"name":"Paul","date":"2015-08-09T01:58:44.950Z","color":255,"alive":true,"lines":["One","Two","Three"],"bonus":"Cadeau"}

Le résultat est intéressant, mais quand on manipule beaucoup JSON et qu'on travaille sur des objets un peu plus complexes les choses se gâtent. Je vous invite à consulter l'exemple qui montre qu'avec des Converter on peut intervenir sur la façon dont le code JSON est générer. Et l'exemple n'explique pas qu'il est possible d'utiliser des attributs comme JSONNameAttribute ou JSONNameAttribute pour influencer tout cela.

Execute.JSON


Et c'est là qu'intervient Execute.JSON, c'est une unité que j'ai écrite pour Delphi XE8 (mais qui devrait fonctionner sur quelques versions précédentes) pour simplifier les choses avec les objectifs suivants:
  1. avoir un code simple à utiliser
  2. avoir un code simple à comprendre
  3. avoir un code pratique à utiliser

Et voici ce que ça donne:

uses
  Execute.JSON;

type
  TSimpleObject = class
    Name : string;
    Date : TDateTime;
    Color: TColor;
    Alive: Boolean;
    Lines: TArray<string>;
    Bonus: string;
  end;
var
  o: TSimpleObject;
begin
  o := TSimpleObject.Create;
  o.Name := 'Paul';
  o.Date := Now();
  o.Color := clRed;
  o.Alive := True;
  o.Lines := ['One', 'Two', 'Three'];
  o.Bonus := 'Cadeau';  

  s := o.toJSON();

  o.Free; // plus besoin de l'objet
end;

et tout cela avec un résultat presque identique - le format date est tronqué - mais c'est un choix personnel puisque de toute façon les dates ne sont pas normalisées dans JSON.
{"name":"Paul","date":"2015-08-09T01:58:44","color":255,"alive":true,"lines":["One","Two","Three"],"bonus":"Cadeau"}

Avouez que c'est plutôt pratique non ?

Mais ce n'est pas tout, Execute.JSON peut jsonifier à peu près n'importe quoi, il faut simplement adapter la syntaxe quand on n'utilise pas un objet:
var
  i: Integer;
  d: TDateTime;
  s: string;
begin
  i := 123;
  s := toJSON(@i, TypeInfo(Integer)); // '123'
  d := Now();
  s := toJSON(@d, TypeInfo(TDateTime)); // "2015-08-09T02:17:20" (oui je sais il est tard !)
end;

Dans la version 2, la syntaxe a été améliorée grâce aux generic !

var
  s: string;
begin
  s := JSON<Integer>.toJSON(123); // '123'
  s := JSON<TDateTime>.toJSON(Now()); // "2015-08-09T02:17:20" (oui je sais il est tard !)
end;

Stefan Glienke m'a fait remarquer alors qu'un tout petit changement permettait de ne pas spécifier le type ! Il suffit de reporter le type generic sur la méthode et de profiter de l'inférence de type !

var
  s: string;
begin
  s := JSON.toJSON(123); // '123'
  s := JSON.toJSON(Now()); // "2015-08-09T02:17:20" (oui je sais il est tard !)
end;


La fonction toJSON a besoin d'un pointer vers la variable et de son TypeInfo, et le tour est joué. Pour les objets un Helper permet de simplifier la syntaxe comme nous l'avons vu ci-dessus

function TObjectHelper.toJSON: string;
begin
  Result := PTypeInfo(ClassType.ClassInfo).toJSON(@Self);
end;

la fonction de classe JSON.<T>toJSON utilise la même astuce.
class function JSON.toJSON(const instance :T): string;
begin
  Result := PTypeInfo(TypeInfo(T)).toJSON(@instance);
end;

L'unité et un programme exemple sont téléchargeables en bas de page, mais je vous donne, en avant goût, les comparaisons faites par la démo entre REST.JSONReflect et Execute.JSON.

type
  TMyValue = (Value1, Value2, Value3);
  TMySet = set of TMyValue;

  TGeneralObject = class
    ti: TTime;
    da: TDate;
    dt: TDateTime;
    mv: TMyValue;
    ms: TMySet;
  end;
var
  o: TGeneralObject;
begin
  o := TGeneralObject.Create;
  o.ti := Now();
  o.da := Now();
  o.dt := Now();
  o.mv := Value2;
  o.ms := [Value1, Value3];
end;

ici, REST oublie de gérer TTime, TDate et le "SET OF".
o.toJSON()
{"ti":"02:23:43","da":"2015-08-09","dt":"2015-08-09T02:23:43","mv":"Value2","ms":["Value1","Value3"]}

m.Marshal(o).toJSON()
{"ti":42225.0998039468,"da":42225.0998039468,"dt":"2015-08-09T02:23:43.061Z","mv":"Value2"}

TStringList

l'objet TStringList est assez commun sous Delphi, voyons ce que donne une propriété Lines: TStringList avec une entrée possédant un objet.
o.toJSON()
{"Lines":["Un","Deux","Trois",{"LostObject":{"Name":null,"Date":null,"Color":0,"Alive":false,"Lines":[],"Bonus":null}}]}

m.Marshal(o).toJSON()
{"lines":{"list":[["Un",null],["Deux",null],["Trois",null],["LostObject",{"name":"","date":"1899-12-30T00:00:00.000Z","color":0,"alive":false,"lines":[],"bonus":""}]],"count":4,"capacity":4,"sorted":false,"duplicates":"dupIgnore","caseSensitive":false,"ownsObject":false,"encoding":null,"defaultEncoding":{"codePage":1252,"mBToWCharFlags":0,"wCharToMBFlags":0,"isSingleByte":true,"maxCharSize":1},"delimiter":"","lineBreak":"","quoteChar":"","nameValueSeparator":"","strictDelimiter":false,"updateCount":0,"writeBOM":true}}


FromJSON()

Evidemment, convertir un objet en JSON n'est pas suffisant, Execute.JSON (tout comme REST) propose la fonction inverse fromJSON() qui permet d'alimenter les champs d'un objet (ou de toute type déclaré par TypeInfo pour Execute.JSON) à partir d'une chaîne JSON. Cela se fait tout aussi simplement:

begin
  o := TGeneralObject.Create();
  o.FromJSON(s);

  // FromJSON(@s, TypeInfo(string), '"Chaîne \"JSON\""'); // obsolete syntax version 1
  // JSON.fromJSON(s,'"Chaîne \"JSON\""'); // obsolete syntax version 2
  JSON.fromJSON(s,'"Chaîne \"JSON\""'); // s = 'Chaîne "JSON"' 
end;

Les Record

Il y a une grosse différence sur la gestion des Record entre REST et Execute.JSON, pour REST un Record est une tableau de valeurs alors que pour Execute.JSON c'est un objet avec des propriétés et des valeurs, exactement comme une classe.
type
  TMyRecord = record
    a : Integer;
    b : string;
  end;
  TMyObject = class
    r : TMyRecord;
  end;

la sérialisation JSON de cet objet sera vraiment différente entre Execute.JSON et REST:
o.toJSON()
{"r":{"a":0,"b":null}}

m.Marshal(o).toJSON()
{"r":[0,""]}

Les erreurs de sérialisation

Pour finir, j'ai relever deux cas où la sérialisation d'un objet pose problème

Premièrement si les informations RTTI ne sont pas disponibles, c'est le cas notamment pour un tableau statique déclaré en ligne:
type
  TError1 = class
    v: array[0..5] of Byte;
  end;

La réaction est différente entre Execute.JSON et REST; Execute.JSON lève un exception indiquant précisément la nature du problème...REST se plante sur une violation d'erreur.
o.toJSON()
No RTTI informations for TError1.v.

m.Marshal(o).toJSON()
Violation d'accès à l'adresse 00455998 dans le module 'JSONDemo.exe'. Lecture de l'adresse 00000004

L'autre cas de figure est la référence circulaire, un objet parent possède un pointeur vers un objet enfant qui lui-même fait référence à son parent. Là encore le résultat est un tout petit peu différent avec possibilité de planter l'application même avec un try/except sous REST.

o.toJSON()
Circular reference in TOwnerObject.Child.TError2.Owner.

m.Marshal(o).toJSON()
Débordement de pile

Les sources sous GPL


L'application démo et l'unité Execute.JSON sont téléchargeables ici, si vous désirez en faire un usage non libre (dans un produit commercial notamment), merci de mon contacter.
zip/JSONDemo.zip
Date de dernière modification : 26/07/2017