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 implementation 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 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.
We will comment on a code sample that will serialize the user-defined types below.
type
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 includes complex collection types (TStringList), sets, arrays, and records. We chose to consider FNumbers a transient field (default for set).
The example uses the following variables:
var m: TJSONMarshal; unm: TJSONUnMarshal;
each instantiated as such:
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 the 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.
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);
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);
For a test code like this:
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);
the intermediate JSON representation 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":[]
}
}
]
}
}
For v: TJSONValue; we can transform a person to and from a JSON object:
... v := m.Marshal(person); ... person := unm.Unmarshal(v); ...
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.