Serializing User Objects

From RAD Studio
Jump to: navigation, search

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.

See Also