Serializing User Objects
Go Up to Developing DataSnap Applications
It is now possible to pass user-defined objects between client and server methods using JSON objects.
The paradigm is based on marshaling user objects into JSON objects and then back into user objects on the opposite side. DataSnap provides a generic serialization suite in the DBXJSONReflect unit through the TTypeMarshaller class. User objects can be transformed into an equivalent representation and then reverted back to user instances based on a suite of converter and reverter classes.
TJSONMarshal and TJSONUnMarshal are two out-of-the-box implementations of the serialization based on JSON objects: user objects are transformed into equivalent JSON objects and those are reverted back into user objects.
Not all user-defined objects can be serialized solely based on the marshaling classes. They may have to be extended with custom converters for user fields that cannot be properly serialized through RTTI. A conversion can be associated with a type or with a field of the user class. There are four types of converters for each category, listed in the following table:
| Description | Field | Type |
|---|---|---|
| Conversion to a string | TStringConverter | TTypeStringConverter |
| Conversion to an object | TObjectConverter | TTypeObjectConverter |
| Conversion to a string array | TStringsConverter | TTypeStringsConverter |
| Conversion to an object array | TObjectsConverter | TTypeObjectsConverter |
The converter/reverter principle is based on data transformation into a representation from which the instance can be restored. One can choose to convert a complex data structure into a simple string that can be parsed to be reverted, or a more sophisticated but more efficient way can be chosen based on the case. Let's say an object collection needs to be serialized. One approach can be to transform each element into a string and concatenate all strings with a unique separator. A more efficient way is to convert the collection into an array of objects that are in it. The reverter receives this array as input and can reconstitute the complex collection.
The code sample listed in this page is split into blocks for easy understanding. The sample is part of a RAD Studio libraries project that you have to create in order to make it run and see the results. The code on this page should be contained in the same source code (.pas file) as the main form of the project.
{$R *.res} uses SysUtils, Classes, JSON, DBXJSON, DBXJSONReflect;
We will comment on a code sample that will serialize the user-defined types below.
type TForm1 = class(TForm) Memo1: TMemo; Button1: TButton; procedure Button1Click(Sender: TObject); procedure MainProc; private { Private declarations } public { Public declarations } end; TAddress = record FStreet: String; FCity: String; FCode: String; FCountry: String; FDescription: TStringList; end; TPerson = class private FName: string; FHeight: integer; FAddress: TAddress; FSex: char; FRetired: boolean; FChildren: array of TPerson; FNumbers: set of 1..10; public constructor Create; destructor Destroy; override; procedure AddChild(kid: TPerson); end;
The example uses the following variables:
var m: TJSONMarshal; unm: TJSONUnMarshal;
The declarations of the TPerson class members:
constructor TPerson.Create; begin FAddress.FDescription := TStringList.Create; end; destructor TPerson.Destroy; begin FAddress.FDescription.Free; inherited; end; procedure TPerson.AddChild(kid: TPerson); begin SetLength(FChildren, Length(FChildren) + 1); FChildren[Length(FChildren) - 1] := kid; end;
The example includes complex collection types (TStringList), sets, arrays, and records. We chose to consider FNumbers a transient field (default for set).
The main program procedure:
procedure TForm1.MainProc; var person, newperson: TPerson; kid: TPerson; JSONString: String; begin m := TJSONMarshal.Create(TJSONConverter.Create); unm := TJSONUnMarshal.Create; { For each complex field type, we will define a converter/reverter pair. We will individually deal with the "FChildren" array, the "TStringList" type, and the "FAddress" record. We will transform the array type into an actual array of "TPerson", as illustrated below. } m.RegisterConverter(TPerson, 'FChildren', function(Data: TObject; Field: String): TListOfObjects var obj: TPerson; I: Integer; begin SetLength(Result, Length(TPerson(Data).FChildren)); I := Low(Result); for obj in TPerson(Data).FChildren do begin Result[I] := obj; Inc(I); end; end); { The implementation is quite straightforward: each child "TPerson" is appended to a predefined type instance "TListOfObjects". Later on, each of these objects will be serialized by the same marshaller and added to a "TJSONArray" instance. The reverter will receive as argument a "TListOfObjects" being oblivious of the "TJSONArray" used for that. } { For "TStringList", we will have a generic converter that can be reused for other marshal instances. The converter simply returns the array of strings of the list. } { Note that this converter is not really needed here because the TStringList converter is already implemented in the Marshaller. } m.RegisterConverter(TStringList, function(Data: TObject): TListOfStrings var i, count: integer; begin count := TStringList(Data).Count; SetLength(Result, count); for I := 0 to count - 1 do Result[i] := TStringList(Data)[i]; end); { Finally, the address record will be transformed into an array of strings, one for each record field with the description content at the end of it. } m.RegisterConverter(TPerson, 'FAddress', function(Data: TObject; Field: String): TListOfStrings var Person: TPerson; I: Integer; Count: Integer; begin Person := TPerson(Data); if Person.FAddress.FDescription <> nil then Count := Person.FAddress.FDescription.Count else Count := 0; SetLength(Result, Count + 4); Result[0] := Person.FAddress.FStreet; Result[1] := Person.FAddress.FCity; Result[2] := Person.FAddress.FCode; Result[3] := Person.FAddress.FCountry; for I := 0 to Count - 1 do Result[4+I] := Person.FAddress.FDescription[I]; end); { It is easy to imagine the reverter's implementation, present below in bulk. } unm.RegisterReverter(TPerson, 'FChildren', procedure(Data: TObject; Field: String; Args: TListOfObjects) var obj: TObject; I: Integer; begin SetLength(TPerson(Data).FChildren, Length(Args)); I := Low(TPerson(Data).FChildren); for obj in Args do begin TPerson(Data).FChildren[I] := TPerson(obj); Inc(I); end end); { Note that this reverter is not really needed here because the TStringList reverter is already implemented in the Unmarshaller. } unm.RegisterReverter(TStringList, function(Data: TListOfStrings): TObject var StrList: TStringList; Str: string; begin StrList := TStringList.Create; for Str in Data do StrList.Add(Str); Result := StrList; end); unm.RegisterReverter(TPerson, 'FAddress', procedure(Data: TObject; Field: String; Args: TListOfStrings) var Person: TPerson; I: Integer; begin Person := TPerson(Data); if Person.FAddress.FDescription <> nil then Person.FAddress.FDescription.Clear else if Length(Args) > 4 then Person.FAddress.FDescription := TStringList.Create; Person.FAddress.FStreet := Args[0]; Person.FAddress.FCity := Args[1]; Person.FAddress.FCode := Args[2]; Person.FAddress.FCountry := args[3]; for I := 4 to Length(Args) - 1 do Person.FAddress.FDescription.Add(Args[I]); end); { The test code is as follows. } person := TPerson.Create; person.FName := 'John Doe'; person.FHeight := 167; person.FSex := 'M'; person.FRetired := false; person.FAddress.FStreet := '62 Peter St'; person.FAddress.FCity := 'TO'; person.FAddress.FCode := '1334566'; person.FAddress.FDescription.Add('Driving directions: exit 84 on highway 66'); person.FAddress.FDescription.Add('Entry code: 31415'); kid := TPerson.Create; kid.FName := 'Jane Doe'; person.AddChild(kid); { Marshal the "person" as a JSONValue and display its contents. } JSONString := m.Marshal(person).ToString; Memo1.Lines.Clear; Memo1.Lines.Add(JSONString); Memo1.Lines.Add('-----------------------'); { Unmarshal the JSONString to a TPerson class } newperson := unm.Unmarshal(TJSONObject.ParseJSONValue(JSONString)) as TPerson; Memo1.Lines.Add(newperson.FName); Memo1.Lines.Add(IntToStr(newperson.FHeight)); { and so on for the other fields } end;
And the button that calls the MainProc:
procedure TForm1.Button1Click(Sender: TObject); begin MainProc; end;
The intermediate JSON representation for the test code in the code sample above is:
{"type":"Converter.TPerson",
"id":1,
"fields":{"FName":"John Doe",
"FHeight":167,
"FAddress":["62 Peter St","TO","1334566","","Driving directions: exit 84 on highway 66","Entry code: 31415"],
"FSex":"M",
"FRetired":false,
"FChildren":[{"type":"Converter.TPerson",
"id":2,
"fields":{"FName":"Jane Doe",
"FHeight":0,
"FAddress":["","","",""],
"FSex":"",
"FRetired":false,
"FChildren":[]
}
}
]
}
}
Serialization success can be checked by making sure that all fields are serialized by checking the marshal's HasWarnings method.
Note: The serialization process solves the circular references.