Direkt zum Hauptbereich

Neuer JSON Serialisierer ab Embarcadero Tokio 10.2

Seit Embarcadero Tokyo 10.2 veröffentlicht hat, steht uns Entwicklern ein neuer sehr gut zu gebrauchender JSON Marshaller zur Verfügung. Leider ist die Dokumentation dürftig. Das schöne ist aber, das Embarcadero sich ziemlich an gängige Verfahren hält. So konnte ich z.B. sehr einfach herausfinden was TJsonCustomCreationConverter<T> macht, nachdem ich C# Beispiele analysierte. Da es leider keine offizielle Dokumentation zum TJsonSerializer gibt, beruhen meine Beispiele auf Versuch und Irrtum.

Es gibt 3 Namespaces, welche relevant sind. Zum einen "System.JSON.Serializers", für den eigentlichen Serialisierer und die Klassenattribute, zum anderen "System.JSON.Converters", für eigene Konverter und zuletzt "System.JSON.Types", für z.B. Datumsformate oder Formatierungen.

TJsonSerializer

Ist die wichtigste Klasse, sie ist implementiert im Namespace System.JSON.Serializers.
Diese Klasse ist dafür zuständig, ein Objekt in JSON (Serialisierung) zu formatieren und ein Objekt durch Lesen bzw. Interpretieren einer JSON-Struktur (Deserialisierung) zu erstellen. Hier mal mein Wrapper:

 unit Nathan.Lib.Marshaller.Json;  
   
 interface  
   
 uses  
  System.JSON.Serializers;  
   
 type  
  TNathanSerializer = class  
  private  
   FSerializer: TJsonSerializer;  
  public  
   constructor Create();  
   destructor Destroy(); override;  
   
   function Deserialize<T>(const data: string): T;  
   function Serialize<T>(const obj: T): string;  
  end;  
   
 implementation  
   
 uses  
  System.JSON.Types;  
   
 { TNathanSerializer }  
   
 constructor TNathanSerializer.Create();  
 begin  
  inherited Create;  
  FSerializer := TJsonSerializer.Create;  
  // FSerializer.Formatting := TJsonFormatting.Indented; // Line Breaks...  
  FSerializer.DateFormatHandling := TJsonDateFormatHandling.Iso;  
 end;  
   
 destructor TNathanSerializer.Destroy();  
 begin  
  FSerializer.Free;  
  inherited;  
 end;  
   
 function TNathanSerializer.Deserialize<T>(const data: string): T;  
 begin  
  Result := FSerializer.Deserialize<T>(data);  
 end;  
   
 function TNathanSerializer.Serialize<T>(const obj: T): string;  
 begin  
  Result := FSerializer.Serialize<T>(obj);  
 end;  
   
 end.  
Mein Wrapper dient dazu, andere JSON Serializer zu Testen. Letztendlich bin ich beim Original geblieben. Ich kann mit meinem Wrapper so vorkonfigurieren, dass das Datumsformat usw. immer gleich ist.
Jetzt aber das Wichtigste, wie nutze ich das ganze um nicht immer alles neu schreiben zu müssen. Bisher hatte ich immer das Problem, dass die Konvertierung in bzw. von JSON immer wieder dem gleichen Muster folgte. In anderen Sprachen war dies schon lange mit Klassenattributen durchzuführen. Nur in Delphi suchte ich nach einem gleichwertigen Framework. So jetzt aber ein Beispiel. Die nachfolgende Klasse ist nur ein Dummy Objekt und soll verschiedene Einsatzmöglichkeiten zeigen.
 unit Nathan.Lib.Marshaller.DummyClass;  
   
 interface  
   
 uses  
  System.JSON.Serializers,  
  System.JSON.Converters,  
  System.Generics.Collections,  
  System.JSON.Writers,  
  System.TypInfo,  
  System.Rtti;  
   
 {$M+}  
   
 type  
  TFooType = (ftNormal, ftRunner, ftFast, ftFaster, ftFastest);  
   
  IAnotherDummy = interface  
   ['{B4FFE2A1-EFAB-4243-9966-8C7DA2E4E29B}']  
   function GetAssembly(): string;  
   procedure SetAssembly(const Value: string);  
   
   property Assembly: string read GetAssembly write SetAssembly;  
  end;  
   
  INathanDummy = interface  
   ['{261C4E59-3DF2-4756-9892-2D7E8C67E1E4}']  
   function GetId(): Integer;  
   function GetMessageBody(): string;  
   function GetInternalType(): TFooType;  
   function GetVersion(): Integer;  
   function GetAnother(): IAnotherDummy;  
   
   procedure SetId(Value: Integer);  
   procedure SetMessageBody(const Value: string);  
   procedure SetInternalType(Value: TFooType);  
   procedure SetVersion(Value: Integer);  
   procedure SetAnother(Value: IAnotherDummy);  
   
   property Id: Integer read GetId write SetId;  
   property MessageBody: string read GetMessageBody write SetMessageBody;  
   property InternalType: TFooType read GetInternalType write SetInternalType;  
   property Version: Integer read GetVersion write SetVersion;  
   property Another: IAnotherDummy read GetAnother write SetAnother;  
  end;  
   
  [JsonSerialize(TJsonMemberSerialization.&In)]  
  TAnotherDummy = class(TInterfacedObject, IAnotherDummy)  
  strict private  
   [JsonIn]  
   [JsonName('Assembly')]  
   FAssembly: string;  
  private  
   function GetAssembly(): string;  
   procedure SetAssembly(const Value: string);  
  public  
   constructor Create();  
  
   property Assembly: string read GetAssembly write SetAssembly;  
  end;  
   
   
  TJsonCustomCreationConverterAnotherDummy = class(TJsonCustomCreationConverter<TAnotherDummy>)  
  protected  
   function CreateInstance(ATypeInf: PTypeInfo): TValue; override;  
  public  
   procedure WriteJson(const AWriter: TJsonWriter; const AValue: TValue; const ASerializer: TJsonSerializer); override;  
  end;  
   
  /// <summary>  
  ///  Demo Class to serialization in json.  
  ///  Another class option for the new de-/serializer:  
  ///   [JsonSerialize(TJsonMemberSerialization.&Public)] Serialize all public fields and properties from my and inherited classes...  
  ///   [JsonSerialize(TJsonMemberSerialization.Fields)]  
  ///   [JsonSerialize(TJsonMemberSerialization.&In)] Just only fields with attribute JsonIn...  
  /// </summary>  
   
  [JsonSerialize(TJsonMemberSerialization.&In)]  
  TNathanDummy = class(TInterfacedObject, INathanDummy)  
  strict private  
   [JsonIgnore] // Ignore this field when serializing...  
   FId: Integer;  
   
   [JsonIn]  
   [JsonName('messagebody')]  
   FMessageBody: string;  
   
   [JsonIn]  
   [JsonName('type')]  
   [JsonConverter(TJsonEnumNameConverter)]  
   FInternalType: TFooType;  
   
   [JsonIn]  
   [JsonName('ver')]  
   FVersion: Integer;  
   
   [JsonIn]  
   [JsonName('Another')]  
   [JsonConverter(TJsonCustomCreationConverterAnotherDummy)]  
   FAnother: IAnotherDummy;  
  private  
   function GetId(): Integer;  
   function GetMessageBody(): string;  
   function GetInternalType(): TFooType;  
   function GetVersion(): Integer;  
   function GetAnother(): IAnotherDummy;  
   
   procedure SetId(Value: Integer);  
   procedure SetMessageBody(const Value: string);  
   procedure SetInternalType(Value: TFooType);  
   procedure SetVersion(Value: Integer);  
   procedure SetAnother(Value: IAnotherDummy);  
  public  
   constructor Create(AId: Integer; const AMessageBody: string; AType: TFooType; AVersion: Integer); overload;  
   destructor Destroy(); override;  
   
   property Id: Integer read GetId write SetId;  
   property MessageBody: string read GetMessageBody write SetMessageBody;  
   property InternalType: TFooType read GetInternalType write SetInternalType;  
   property Version: Integer read GetVersion write SetVersion;  
   property Another: IAnotherDummy read GetAnother write SetAnother;  
  end;  
   
 {$M-}  
   
 implementation  
     
 { TAnotherDummy }  
   
 constructor TAnotherDummy.Create;  
 begin  
  inherited;  
  FAssembly := 'Baugruppe';  
 end;  
   
 function TAnotherDummy.GetAssembly: string;  
 begin  
  Result := FAssembly;  
 end;  
   
 procedure TAnotherDummy.SetAssembly(const Value: string);  
 begin  
  FAssembly := Value;  
 end;  
    
 { TJsonCustomCreationConverterAnotherDummy }  
   
 function TJsonCustomCreationConverterAnotherDummy.CreateInstance(ATypeInf: PTypeInfo): TValue;  
 begin  
  Result := TAnotherDummy.Create;  
 end;  
   
 procedure TJsonCustomCreationConverterAnotherDummy.WriteJson(  
  const AWriter: TJsonWriter; const AValue: TValue;  
  const ASerializer: TJsonSerializer);  
 var  
  AnotherDummy: IAnotherDummy;  
 begin  
  if AValue.TryAsType(AnotherDummy) then  
   ASerializer.Serialize(AWriter, AnotherDummy as TAnotherDummy);  
 end;  
      
 { TNathanDummy }  
   
 constructor TNathanDummy.Create(AId: Integer; const AMessageBody: string; AType: TFooType; AVersion: Integer);  
 begin  
  inherited Create;  
  FId := AId;  
  FMessageBody := AMessageBody;  
  FInternalType := AType;  
  FVersion := AVersion;  
  FAnother := TAnotherDummy.Create;  
 end;  
   
 destructor TNathanDummy.Destroy();  
 begin  
  FAnother := nil;  
  inherited;  
 end;  
   
 function TNathanDummy.GetAnother: IAnotherDummy;  
 begin  
  Result := FAnother;  
 end;  
   
 function TNathanDummy.GetId: Integer;  
 begin  
  Result := FId;  
 end;  
   
 function TNathanDummy.GetInternalType: TFooType;  
 begin  
  Result := FInternalType;  
 end;  
   
 function TNathanDummy.GetMessageBody: string;  
 begin  
  Result := FMessageBody;  
 end;  
   
 function TNathanDummy.GetVersion: Integer;  
 begin  
  Result := FVersion;  
 end;  
   
 procedure TNathanDummy.SetAnother(Value: IAnotherDummy);  
 begin  
  FAnother := Value;  
 end;  
   
 procedure TNathanDummy.SetId(Value: Integer);  
 begin  
  FId := Value;  
 end;  
   
 procedure TNathanDummy.SetInternalType(Value: TFooType);  
 begin  
  FInternalType := Value;  
 end;  
   
 procedure TNathanDummy.SetMessageBody(const Value: string);  
 begin  
  FMessageBody := Value;  
 end;  
   
 procedure TNathanDummy.SetVersion(Value: Integer);  
 begin  
  FVersion := Value;  
 end;  
   
 end.  

Da ich sehr viel mit Interfaces arbeite, wollte ich das jede Implementation einer Klasse von einem Interface abstammt. So sollte INathanDummy ein Property auf IAnotherDummy haben und nicht auf TAnotherDummy.

[JsonSerialize(TJsonMemberSerialization.&In)] 

Das Klassenattribut bestimmt, dass nur Felder mit dem Attribut [JsonIn] berücksichtigt werden. Analog könnte man auch das Attribut [JsonIgnore] verwenden, welches dann bedeutet, dass das Property nicht vom Marshaller berücksichtigt wird.

[JsonName('messagebody')] 

Bestimmt den Bezeichner im JSON. Fehlt er, heißt der Bezeichner wie das Feld. Ist zwar Ok, sollte nur zwischen den eigenen Programmen JSON Daten ausgetauscht werden, aber wann kommt dies vor. Frontend ist schnell und einfach in Delphi geschrieben. Backend kann aber mit einer ganz anderen Sprache implementiert sein.

[JsonConverter(TJsonEnumNameConverter)] 

Konvertiert mir mein Enum TFooType in den Plaintext Namen. Sonst steht im JSON nur so was wie {"type":1} anstelle von {"type":"ftRunner"}, ist doch viel leserlicher und jeder Programmierer weiss sofort was gemeint ist.

[JsonConverter(TJsonCustomCreationConverterAnotherDummy)]

Bevor ich den TJsonCustomCreationConverter<T> verwendete, war das Property "FAnother" immer leer im JSON, obwohl ich es im Konstruktor initialisierte. Um eine Klassenproperty zu serialisieren, brauchte ich einen eigenen "JSON Custom Creation Converter". Das Attribut bestimmt, welcher Converter für das Klassenproperty zu verwenden ist. In meinem Fall brauchte ich eine Implementation von CreateInstance() um das Unmarshalling also JSON to Object durchzuführen und eine Implementation von WriteJson() um das Marshalling durchzuführen.

Conclusion

Hoffe ich konnte damit Helfen die Dokumentation mit Beispielen zu ergänzen. Test, voller Sourcecode usw. findet man auf Github unter: https://github.com/Thurnreiter/GeneralStuff/tree/master/Thurnreiter.Lib

Meiner Meinung nach, hat Embarcadero ein voll nutzbares JSON Framework herausgebracht, hoffentlich kommen noch ein paar brauchbare TConverter mit den nächsten Versionen hinzu.

Vielen Dank...

Kommentare

Beliebte Posts aus diesem Blog

Delphi mit Facebook per OAuth2 verbinden

Als ich mich das erste mal mit OAuth2 zur Autorisierung beschäftigte, begann ich mit der RESTDemo.exe von Delphi zu experimentieren. Dies hat aber nicht so einfach funktioniert wie ich dachte. Also beschreibe ich hier mal mein Vorgehen wie ich mich mit Facebook Autorisieren kann. Die Vorarbeit liegt nicht unbedingt auf Seite Delphi, sondern genauso auf der Seite Facebook. Registrierung Facebook Als erstes muss man seine APP auf  https://developers.facebook.com/apps  registrieren. Als nächstes, geht man auf Einstellungen - Allgemeines und holt sich die "App-ID", in Delphi "Client-ID" und "Client-Secret", bzw. "App-Geheimcode". Beides ist später für die Url nötig. Wichtig in dem Zusammenhang, als "App Domain" "localhost" einzutragen. Um den Status der App auf Live zu stellen ist von Seite Facebook ein Links zur Datenschutzrichtlinie erforderlich. Hatte hierfür die Url meines Arbeitgebers eingetragen. Um die App Live zu s...

Wer ist Nathan Thurnreiter

Das bin ich Nathan Thurnreiter. Ein mit Leib und Seele begeisterter Delphi Programmierer. Viele Entwickler von heute kennen sich sehr gut aus wie man schnell was erstellt und ändert. Aber können Prinzipien wie SOLID oder LOD einfach nicht richtig anwenden, bzw. haben es nicht verinnerlicht. Dies bewegte mich endlich, selbst einen Blog zu eröffnen und den mal in deutsch. Nun aber mal ein bisschen zu mir. Bin über Umwege zur Informatik gestossen. Hatte zuerst Automobilmechaniker gelernt und dann Jahre beim Studium von Maschienenbau/Fahrzeugtechnik und technischer Physik verbracht, biss ich dann zum Programmieren kam. Dies begann mit Pascal bzw. Delphi und ich bin fast ausschliesslich dabei geblieben. Natürlich habe ich wie jeder Programmierer mal über den Tellerrand geschaut. Aber bis auf ein paar nervige Kleinigkeiten ist Delphi für mich die Sprache schlecht hin. Momentan leite ich ein kleines Entwickler Team, welches mit C# und Delphi ein Warenwirtschaftssystem erstellt. Die nächste...