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

MVC mit System.Messaging

In der Vergangenheit hatte ich, beim Einsatz vom MVC Pattern immer eine eigene Implementation des Observer Pattern um Nachrichten vom Model ans View zu schicken. Hierbei handelte es sich um ein einfaches Record welches nur Text verschicken konnte. Das View musste deswegen auch immer das Model, bzw. das Interface des Model kennen um Daten für Aktualisierung zu haben. Dies widerspricht meiner Meinung nach, dem Prinzip von Clean Code "Separation of Concerns" und koppelt das View ans Model. Um dem entgegen zu wirken, experimentierte ich mit den Board-Mitteln Delphi, aus dem Namespace System.Messaging . Im View hänge ich mich an den DefaultManager der Klasse TMessageManager . procedure TFormShowMessage.FormCreate(Sender: TObject); begin FSubscriptionId := TMessageManager.DefaultManager.SubscribeToMessage(TMessage , procedure(const Sender: TObject; const AMessage: TMessage) begin Memo1.Lines.Add((AMessage as TMessage ).Value); end); end; Das Model schickt ...

Wie setzte ich Mocking Frameworks ein um meine Logik zu Testen?

Allgemein Bisher hatte ich zum Mocken meist das  Delphi-Mocks Framework  verwendet, was in den meisten Fällen ausreichte. In dem Post zeige ich, wie man das Mocking von Spring4D verwendet. Der Grund für diesen Post ist, dass es immer noch genügend Programmierer gibt welche noch nie oder viel zu selten Tests schreiben oder einsetzen. Viele reden darüber und erzählen, dass sie "natürlich" Testen. Aber ohne automatische Tests kann man die Qualität sowie einen sauberen Code nicht gewährleisten. TDD (test driven development) hat seine Schattenseiten, aber für jemand der sich nie richtig mit Clean Code, SOLID oder ähnlichem beschäftigt hat, hilft es die Prinzipien einfacher einzuhalten. In jeder Programmiersprache kann ein sauber Code geschrieben werden. Langfristig ist ein schneller, aber unsauberer Code, teurer als einmalig einen sauberen Code zu schreiben. Diese Einsicht ist meiner Erfahrung nach, leider noch nicht bei allen Programmierern angekommen. Tests dienen dazu, da...