Custom Marshalling/UnMarshalling in Delphi 2010

Delphi 2010, Programming, RTTI, Uncategorized Add comments

Introduction
Some days ago, Embarcadero has presented the new version of RAD Studio, 2010.
The are many new features, but you can find in a lot places around the web, so
I won’t repeat them here.

One of the things widely requested from all Delphi programmers all over the world over the past few years, including myself, is
certainly a new and more powerful RTTI.

The new system of RTTI has finally arrived, and pave the way for a large number of applications.
One area that has benefited from the new RTTI is for sure the marshaled objects.

Marshaling is defined as follows:

“In computer science, marshalling (similar to serialization) is the process of
transforming the memory representation of an object to a data format suitable for
storage or transmission. It is typically used when data must be moved between
different parts of a computer program or from one program to another.
The opposite, or reverse, of marshalling is called unmarshalling (demarshalling) (similar to deserialization).”
–WikiPedia

In Delphi 2010 the process of serialization and deserialization is handled respectively by a Marshaller and an Unmarshaller.

The built-in format for the serialization of any Delphi object is JSON.
There are 2 main classes responsible for serializing objects into JSON, both present in the unit DBXJSONReflect:
- TJSONMarshal
- TJSONUnMarshal

Let’s say you have an object defined as follow:

type
  1.   TKid = class
  2.     FirstName: String;
  3.     LastName: String;
  4.     Age: Integer;
  5.   end;

To serialize and deserialize an instance of TKid it requires the following steps:

var
  1.   Mar: TJSONMarshal;  //Serializer
  2.   UnMar: TJSONUnMarshal;  //UnSerializer
  3.   Kid: TKid;  //The Object to serialize
  4.   SerializedKid: TJSONObject;  //Serialized for of object
  5. begin
  6.   Mar := TJSONMarshal.Create(TJSONConverter.Create);
  7.   try
  8.     Kid := TKid.Create;
  9.     try
  10.       Kid.FirstName := 'Daniele';
  11.       Kid.LastName := 'Teti';      
  12.       Kid.Age := 29;      
  13.       SerializedKid := Mar.Marshal(Kid) as TJSONObject;
  14.     finally
  15.       FreeAndNil(Kid);
  16.     end;
  17.   finally
  18.     Mar.Free;
  19.   end;
  20.   //Output the JSON version of the Kid object
  21.   WriteLn(SerializedKid.ToString);  
  22.   // UnMarshalling Kid
  23.   UnMar := TJSONUnMarshal.Create;
  24.   try
  25.     Kid := UnMar.UnMarshal(SerializedKid) as TKid;
  26.     try
  27.       //now kid is the same as before marshalling
  28.       Assert(Kid.FirstName = 'Daniele');
  29.       Assert(Kid.LastName = 'Teti');
  30.       Assert(Kid.Age = 29);
  31.     finally
  32.       Kid.Free;
  33.     end;
  34.   finally
  35.     UnMar.Free;
  36.   end;
  37. end;

Simple, isn’t it?
To access the JSON string that is our object, we must call the method ToString.
The JSON representation of this object SerializedKid can be saved to file,
sent to a remote server, used by a Web page from a web service, stored on a database or sent into space (!!!).
The Delphi application re-read the JSON string, you can recreate the object as it was at the time of serialization.
But anyone with a JSON parser can still read the data in our object, even non Delphi client.
These are the advantages of having used an open format and standard.

So far the simple part …
How serialize a field differently from the default?

Suppose we add the date of birth to our TKid:

type
  1.   TKid = class
  2.     FirstName: String;
  3.     LastName: String;
  4.     Age: Integer;
  5.     BornDate: TDateTime;
  6.   end;

Serialize a TDateTime, localized and that I have in JSON string is a float, because for Delphi TDateTime is a decimal number.
If I read the data from another program Delphi, no problem, but if I wanted to read a script in JavaScript? or. NET? or Ruby?
Then I use a format “DATA” to understand, even for these languages.
The new engine provides the serialization too.
Is needed, however, to tell the Marshaller and UnMarsheller how to represent and reconstruct a particular
object field by two statements like the following:

//marshaller
  1. Marshaller.RegisterConverter(TKid, 'BornDate',
  2.   function(Data: TObject; Field: string): string
  3.   var
  4.     ctx: TRttiContext; date : TDateTime;
  5.   begin
  6.     date := ctx.GetType(Data.ClassType).GetField(Field).GetValue(Data).AsType<TDateTime>;
  7.     Result := FormatDateTime('yyyy-mm-dd hh:nn:ss', date);
  8.   end);
  9.  
  10. //UnMarshaller
  11. UnMarshaller.RegisterReverter(TKid, 'BornDate',
  12.   procedure(Data: TObject; Field: string; Arg: string)
  13.   var
  14.     ctx: TRttiContext;
  15.     datetime:TDateTime;
  16.   begin
  17.     datetime := EncodeDateTime(StrToInt(Copy(Arg, 1, 4)),
  18.                                StrToInt(Copy(Arg, 6, 2)),
  19.                                StrToInt(Copy(Arg, 9, 2)),
  20.                                StrToInt(Copy(Arg, 12, 2)),
  21.                                StrToInt(Copy(Arg, 15, 2)),
  22.                                StrToInt(Copy(Arg, 18, 2)), 0);
  23.     ctx.GetType(Data.ClassType).GetField(Field).SetValue(Data, datetime);
  24.   end);

The anonymous method is called when the marshaller serializes the field ‘BornDate’ is called “Converter” while Unmarshaller anonymous method that calls when he has to reconstruct the object from the JSON string is the “Reverter”.
Thus serializing a TKid assure you that my object is readable both by Delphi from another language without loss of information.

But what happens when I have to serialize a complex type?

Suppose we extend TKid this:

type
  1.   TTeenager = class(TKid)
  2.     Phones: TStringList;
  3.     constructor Create; virtual;
  4.     destructor Destroy; virtual;
  5.   end;

We must define a Converter and a Reverter for the TStringList class.
We can do it this way:

var
  1.   Marshaller: TJSONMarshal;
  2.   UnMarshaller: TJSONUnMarshal;
  3.   Teenager: TTeenager;
  4.   Value, JSONTeenager: TJSONObject;
  5. begin
  6.   Marshaller := TJSONMarshal.Create(TJSONConverter.Create);
  7.   try
  8.     Marshaller.RegisterConverter(TTeenager, 'BornDate',
  9.       function(Data: TObject; Field: string): string
  10.       var
  11.         ctx: TRttiContext; date : TDateTime;
  12.       begin
  13.         date := ctx.GetType(Data.ClassType).GetField(Field).GetValue(Data).AsType<TDateTime>;
  14.         Result := FormatDateTime('yyyy-mm-dd hh:nn:ss', date);
  15.       end);
  16.      
  17.     Marshaller.RegisterConverter(TStringList, function(Data: TObject): TListOfStrings
  18.                                               var
  19.                                                 i, count: integer;
  20.                                               begin
  21.                                                 count := TStringList(Data).count;
  22.                                                 SetLength(Result, count);
  23.                                                 for i := 0 to count - 1 do
  24.                                                   Result[i] := TStringList(Data)[i];
  25.                                               end);  //TStringList Converter
  26.     Teenager := TTeenager.CreateAndInitialize;
  27.     try
  28.       Value := Marshaller.Marshal(Teenager) as TJSONObject;
  29.     finally
  30.       Teenager.Free;
  31.     end;
  32.   finally
  33.     Marshaller.Free;
  34.   end;
  35.   // UnMarshalling Teenager
  36.   UnMarshaller := TJSONUnMarshal.Create;
  37.   try
  38.     UnMarshaller.RegisterReverter(TTeenager, 'BornDate',
  39.       procedure(Data: TObject; Field: string; Arg: string)
  40.       var
  41.         ctx: TRttiContext;
  42.         datetime: TDateTime;
  43.       begin
  44.         datetime := EncodeDateTime(StrToInt(Copy(Arg, 1, 4)),
  45.                                    StrToInt(Copy(Arg, 6, 2)),
  46.                                    StrToInt(Copy(Arg, 9, 2)),
  47.                                    StrToInt(Copy(Arg, 12, 2)),
  48.                                    StrToInt(Copy(Arg, 15, 2)),
  49.                                    StrToInt(Copy(Arg, 18, 2)), 0);
  50.         ctx.GetType(Data.ClassType).GetField(Field).SetValue(Data, datetime);
  51.       end);
  52.     UnMarshaller.RegisterReverter(TStringList, function(Data: TListOfStrings): TObject
  53.                                                var
  54.                                                  StrList: TStringList;
  55.                                                  Str: string;
  56.                                                begin
  57.                                                  StrList := TStringList.Create;
  58.                                                  for Str in Data do
  59.                                                    StrList.Add(Str);
  60.                                                  Result := StrList;
  61.                                                end);  //TStringList Reverter
  62.  
  63.     Teenager := UnMarshaller.Unmarshal(Value) as TTeenager;
  64.     try
  65.       Assert('Daniele' = Teenager.FirstName);
  66.       Assert('Teti' = Teenager.LastName);
  67.       Assert(29 = Teenager.Age);
  68.       Assert(EncodeDate(1979, 11, 4) = Teenager.BornDate);
  69.       Assert(3 = Teenager.Phones.Count);
  70.       Assert('NUMBER01'=Teenager.Phones[0]);
  71.       Assert('NUMBER02'=Teenager.Phones[1]);
  72.       Assert('NUMBER03'=Teenager.Phones[2]);
  73.     finally
  74.       Teenager.Free;
  75.     end;
  76.   finally
  77.     UnMarshaller.Free;
  78.   end;
  79. end;

There are different types of Converter and Reverter.
In the the DBXJSONReflect there are 8 types of converters:

  1.   //Convert a field in an object array
  2.   TObjectsConverter = reference to function(Data: TObject; Field: String): TListOfObjects;
  3.   //Convert a field in a strings array
  4.   TStringsConverter = reference to function(Data: TObject; Field: string): TListOfStrings;
  5.  
  6.   //Convert a type in an objects array
  7.   TTypeObjectsConverter = reference to function(Data: TObject): TListOfObjects;
  8.   //Convert a type in a strings array  
  9.   TTypeStringsConverter = reference to function(Data: TObject): TListOfStrings;
  10.  
  11.   //Convert a field in an object
  12.   TObjectConverter = reference to function(Data: TObject; Field: String): TObject;
  13.   //Convert a field in a string  
  14.   TStringConverter = reference to function(Data: TObject; Field: string): string;
  15.  
  16.   //Convert specified type in an object
  17.   TTypeObjectConverter = reference to function(Data: TObject): TObject;
  18.   //Convert specified type in a string  
  19.   TTypeStringConverter = reference to function(Data: TObject): string;

Each of them deals with a particular conversion object representation in the final serialization, in our case we will use them to convert to JSON.

Also in the DBXJSONReflect unit are defined many “Reverter” dealing with retrieving
the serialized version of the data and use it to reconstruct the object previously serialized.
Because they are complementary to the Converter, I will not copy them here.

As a final example, we derive from TProgrammer by TTeenager adding a list of Laptops in the properties.

Is therefore necessary to introduce a new pair of Converter / Reverter.
In this example I have defined all the converter and reverter in another unit in
order to have more readable code:

type
  1.   TLaptop = class
  2.     Model: String;
  3.     Price: Currency;
  4.     constructor Create(AModel: String; APrice: Currency);
  5.   end;
  6.   TLaptops = TObjectList<TLaptop>;
  7.   TProgrammer = class(TTeenager)
  8.     Laptops: TLaptops;
  9.     constructor Create; override;
  10.     destructor Destroy; override;
  11.     class function CreateAndInitialize: TProgrammer;
  12.   end;
  13. // Implementation code…
  14. var
  15.   Marshaller: TJSONMarshal;
  16.   UnMarshaller: TJSONUnMarshal;
  17.   Programmer: TProgrammer;
  18.   Value, JSONProgrammer: TJSONObject;
  19. begin
  20.   Marshaller := TJSONMarshal.Create(TJSONConverter.Create);
  21.   try
  22.     Marshaller.RegisterConverter(TProgrammer, 'BornDate', ISODateTimeConverter);
  23.     Marshaller.RegisterConverter(TStringList, StringListConverter);
  24.     Marshaller.RegisterConverter(TProgrammer, 'Laptops', LaptopListConverter);
  25.     Programmer := TProgrammer.CreateAndInitialize;
  26.     try
  27.       Value := Marshaller.Marshal(Programmer) as TJSONObject;
  28.     finally
  29.       Programmer.Free;
  30.     end;
  31.  
  32.     // UnMarshalling Programmer
  33.     UnMarshaller := TJSONUnMarshal.Create;
  34.     try
  35.       UnMarshaller.RegisterReverter(TProgrammer, 'BornDate', ISODateTimeReverter);
  36.       UnMarshaller.RegisterReverter(TStringList, StringListReverter);
  37.       UnMarshaller.RegisterReverter(TProgrammer, 'Laptops', LaptopListReverter);
  38.  
  39.       Programmer := UnMarshaller.Unmarshal(Value) as TProgrammer;
  40.       try
  41.         Assert('Daniele' = Programmer.FirstName);
  42.         Assert('Teti' = Programmer.LastName);
  43.         Assert(29 = Programmer.Age);
  44.         Assert(EncodeDate(1979, 11, 4) = Programmer.BornDate);
  45.         Assert(3 = Programmer.Phones.Count);
  46.         Assert('NUMBER01' = Programmer.Phones[0]);
  47.         Assert('NUMBER02' = Programmer.Phones[1]);
  48.         Assert('NUMBER03' = Programmer.Phones[2]);
  49.         Assert('HP Presario C700' = Programmer.Laptops[0].Model);
  50.         Assert(1000 = Programmer.Laptops[0].Price);
  51.         Assert('Toshiba Satellite Pro' = Programmer.Laptops[1].Model);
  52.         Assert(800 = Programmer.Laptops[1].Price);
  53.         Assert('IBM Travelmate 500' = Programmer.Laptops[2].Model);
  54.         Assert(1300 = Programmer.Laptops[2].Price);
  55.       finally
  56.         Programmer.Free;
  57.       end;
  58.     finally
  59.       UnMarshaller.Free;
  60.     end;
  61.   finally
  62.     Marshaller.Free;
  63.   end;
  64. end;

Unit CustomConverter.pas contains all needed Converters/Reverts as anon methods.

unit CustomConverter;
  1.  
  2. interface
  3.  
  4. uses
  5.   DBXJSONReflect,
  6.   MyObjects; //Needed by converter and reverter for TLaptops
  7.  
  8. var
  9.   ISODateTimeConverter: TStringConverter;
  10.   ISODateTimeReverter: TStringReverter;
  11.  
  12.   StringListConverter: TTypeStringsConverter;
  13.   StringListReverter: TTypeStringsReverter;
  14.  
  15.   LaptopListConverter: TObjectsConverter;
  16.   LaptopListReverter: TObjectsReverter;
  17.  
  18. implementation
  19.  
  20. uses
  21.   SysUtils, RTTI, DateUtils, Classes;
  22.  
  23. initialization
  24.  
  25. LaptopListConverter := function(Data: TObject; Field: String): TListOfObjects
  26. var
  27.   Laptops: TLaptops;
  28.   i: integer;
  29. begin
  30.   Laptops := TProgrammer(Data).Laptops;
  31.   SetLength(Result, Laptops.Count);
  32.   if Laptops.Count > 0 then
  33.     for I := 0 to Laptops.Count - 1 do
  34.       Result[I] := Laptops[i];
  35. end;
  36.  
  37.  
  38. LaptopListReverter := procedure(Data: TObject; Field: String; Args: TListOfObjects)
  39. var
  40.   obj: TObject;
  41.   Laptops: TLaptops;
  42.   Laptop: TLaptop;
  43.   i: integer;
  44. begin
  45.   Laptops := TProgrammer(Data).Laptops;
  46.   Laptops.Clear;
  47.   for obj in Args do
  48.   begin
  49.     laptop := obj as TLaptop;
  50.     Laptops.Add(TLaptop.Create(laptop.Model, laptop.Price));
  51.   end;
  52. end;
  53.  
  54. StringListConverter := function(Data: TObject): TListOfStrings
  55. var
  56.   i, count: integer;
  57. begin
  58.   count := TStringList(Data).count;
  59.   SetLength(Result, count);
  60.   for i := 0 to count - 1 do
  61.     Result[i] := TStringList(Data)[i];
  62. end;
  63.  
  64.  
  65. StringListReverter := function(Data: TListOfStrings): TObject
  66. var
  67.   StrList: TStringList;
  68.   Str: string;
  69. begin
  70.   StrList := TStringList.Create;
  71.   for Str in Data do
  72.     StrList.Add(Str);
  73.   Result := StrList;
  74. end;
  75.  
  76. ISODateTimeConverter := function(Data: TObject; Field: string): string
  77. var
  78.   ctx: TRttiContext; date : TDateTime;
  79. begin
  80.   date := ctx.GetType(Data.ClassType).GetField(Field).GetValue(Data).AsType<TDateTime>;
  81.   Result := FormatDateTime('yyyy-mm-dd hh:nn:ss', date);
  82. end;
  83.  
  84. ISODateTimeReverter := procedure(Data: TObject; Field: string; Arg: string)
  85. var
  86.   ctx: TRttiContext;
  87.   datetime :
  88.   TDateTime;
  89. begin
  90.   datetime := EncodeDateTime(StrToInt(Copy(Arg, 1, 4)), StrToInt(Copy(Arg, 6, 2)), StrToInt(Copy(Arg, 9, 2)), StrToInt
  91.       (Copy(Arg, 12, 2)), StrToInt(Copy(Arg, 15, 2)), StrToInt(Copy(Arg, 18, 2)), 0);
  92.   ctx.GetType(Data.ClassType).GetField(Field).SetValue(Data, datetime);
  93. end;
  94.  
  95. end.

Last hint…
Every serialization/unserialization process can create “warnings”.
Those warnings are collected into the “Warnings” property of the Ser/UnSer Object.

Conclusions
In this post I tried to introduce the basics of the new serialization engine in Delphi 2010.
During the next ITDevCon to be held in Italy next November 11.12, I’ll have a talk in which I will extensively talk about serialization and RTTI.
All interested smart developers are invited :-)

ITALIAN P.S.
Se qualche programmatore italiano volesse avere la versione in italiano di questo post può lasciare un commento e vedrò di accontentarlo :-)

You can find the DUnit project Source Code

20 Responses to “Custom Marshalling/UnMarshalling in Delphi 2010”

  1. visli Says:

    Very good article, I like your writing style. thorough and easy to understand.

  2. FrancisR Says:

    Nice article. regards. Very clear description.

  3. Paweł Głowacki Says:

    Very interesting reading and useful code snippets!
    Nice one, Daniele!

  4. Chris Says:

    Excellent article,
    Easy to understand.
    Thanks Daniele !!!

  5. Paweł Głowacki : Delphi 2010 Launch in the Netherlands and my Top 3 blog posts Says:

    [...] Custom Marshalling/UnMarshalling in Delphi 2010. Daniele Teti demonstrates the details of Delphi 2010 new support from converting Delphi objects to [...]

  6. Nick Hodges » Blog Archive » Random Thoughts on the Passing Scene #125 Says:

    [...] Cool things you can do with Delphi 2010 #221:  Marshall and unmarshall objects using JSON. [...]

  7. LDS Says:

    This example shows a basic flaw of JSON as used by Delphi: it is very easy to sniff and tamper with data. And Datasnap does nothing to protect the data and identify who sent them. Sure, one can use IPSec (which may require a fairly complex network setup, and can identify a machine, not a user) or TLS/SSL (and it requires certificate issuing and management for all parties). Would you transport your sensitive data via a string protocol with no protection?
    Also, all those strings manipulations reminds me of PHP, not a compiled language very apt to manage binary streams. But one day all data types will go away and programmers will be able to use strings only…

  8. Claudio Piffer Says:

    great article! very useful!!

    Thanks Daniele

  9. Daniele Teti Says:

    @LDS:
    From JSON website:
    “JSON is a lightweight data-interchange format. It is easy for humans to read and write. It is easy for machines to parse and generate.”
    Those are all the JSON benefits. JSON is not related with security issues like XML (as data-interchange protocol) is not directly related with SOAP web services security issues. Security is not a responsability of the data-interchage format, but is a responsability for the transport protocol.

    Wikipedia about transport protocol says:
    “In computer networking, the Transport Layer is a group of methods and protocols within a layered architecture of network components within which it is responsible for encapsulating application data blocks into data units (datagrams, segments) suitable for transfer to the network infrastructure for transmission to the destination host”

    This is definitively not a JSON responsability.

    However, your security needs (very similar to my needs for some projects) are very important… so I’m preparing some talk about datasnap and datasnap filters. I’m developing a cryptography filter for secure comunication over DataSnap (without using “TCP security” tecnologies like IPSec). Probably I’ll talk about this filters in the italian Delphi2010 presentation in Milan next september-24-2010 and, in a more deeper manner, at the ITDevCon in Verona 11-12 sept 2010.

  10. scoluccia Says:

    Great article!!

    in the first example (serialize and deserialize an instance of TKid) if I save the SerializedKid.ToString into a file when I reload the file how can I convert the string into a TJsonValue (SerializedKid) to call UnMar.UnMarshal(SerializedKid) as TKid;

    I’ve tried SerializedKid:= TJSONString.Create(Memo1.Lines.LoadfFromFile(’xxx.txt’);
    but I get an access violation

    There are very few examples on the web about Json in delphi 2010
    Thanks for your support!!!

  11. Daniele Teti Says:

    @scoluccia:
    You can find a specific example for file persistence in this other post http://www.danieleteti.it/?p=189

    –daniele

  12. Alin Sfetcu Says:

    those converters/reverts need to be free/released or something ?
    Because i get a memory leak as long as they are defined. Just open/close the application, without using the converters/reverts will result in a memory leak.

    thank you

  13. Michael Justin Says:

    Does the marshaling/unmarshaling work with private (or protected) fields too? In the example it looks like only published properties can be processed …

  14. Massimiliano Trezzi Says:

    StringListReverter causes a memory leak!
    Everytime the callback is called during the unmarshal process it replaces the existing field with a new instance without freeing the old one!
    If I understood all, this is a design problem with all TTypeXXXReverter callbacks.
    Did I understood?

    Thank you.

    #
    StringListReverter := function(Data: TListOfStrings): TObject
    #
    var
    #
    StrList: TStringList;
    #
    Str: string;
    #
    begin
    #
    StrList := TStringList.Create;
    #
    for Str in Data do
    #
    StrList.Add(Str);
    #
    Result := StrList;
    #
    end;

  15. Massimiliano Trezzi Says:

    I investigated and I must say that “Delphi JSON Marshaling/Unmarshaling” is full of bugs like this.
    Look in QualityCentral: #QC#81869, QC#81862 and QC#82857.
    I’m very disappointed.
    How can I use a feature with bugs like those?

    Thank you.

  16. Alex Gordon Says:

    Я считаю, что Вы допускаете ошибку. Пишите мне в PM, пообщаемся….

    Introduction
    Some days ago, Embarcadero has presented the new version of RAD Studio, 2010…..

  17. Wagner Freitas Says:

    About this article
    I’ve been studying about patterns datetime
    I found http://www.w3.org/TR/NOTE-datetime

  18. Hugo Says:

    I tested the code in Delphi EX, and it does not work, why?

  19. Bob Florian Says:

    Excellent post!

    How can I marshall a TDictionary in Delphi 2010? I can’t seem to get it to call the registered converter at all. Is this because Delphi 2010’s RTTI doesn’t know about the instantiated generics? Otherwise i’ll be stuck using customized xml marshalling for now.

  20. json encode mit Delphi erzeugen - Delphi-PRAXiS Says:

    [...] AW: json encode mit Delphi erzeugen   Heute, 09:40 Entschuldige mein l

Leave a Reply

You must be logged in to post a comment.

WP Theme & Icons by N.Design Studio
Entries RSS Comments RSS Log in