Working with JSON in Delphi

Many legacy systems are stuck in old versions of Delphi, which means that often developers are faced with the challenge of making decades old code do things it was never ever designed to. In my case, the project I’m working on is written in Delphi 6, and the challenge I face is making it parse JSON messages.

The Synopse mORMot Framework is a powerful open source framework that is compatible with older versions of Delphi and allows you to create a self-hosted RESTful API complete with an ORM. Yet, you don’t have to use its entire range of features to benefit from mORMot. Indeed, I have found its routines for working with JSON particularly helpful in my situation.

JSON documents as variants

While mORMot offers the functionality to serialize and deserialize objects (more on this later), I wanted to manipulate my JSON objects more directly. Fortunately, this too is possible in mORMot, through the user of the _Json method and the variant type.

You can pass your JSON text as a string to the _Json method (found in SynCommons.pas). What it returns is a variant representation of the object that can be navigated using the . (dot) operator, as one would in JavaScript.

For example, given the following JSON:

{
"Name": "Piet Pompies",
"GraduationYear": 2018,
"Address":
{
"Street": "1 Learner Way",
"City": "Schoolburg",
"Country": "Academia"
}
}

You could do this:

var
Student: Variant;
begin
Student := _Json(InputStr);
Writeln(Student.Name); // Piet Pompies
Writeln(Student.GraduationYear); // 2018
Writeln(Student.Address.Street); // 1 Learner Way
Writeln(Student.Address.City); // Schoolburg
Writeln(Student.Address.Country); // Academia
end.

To parse arrays in this way, I needed to get my JSON into the TDocVariantData type. This can be done by calling the DocVariantData method and passing in your variant(this method actually returns a pointer type PDocVariantData). This class has many useful methods that you can use to navigate your object.

For example, given the following JSON:

{
"Order":
{
"OrderNo": "00001",
"OrderItems":
[
{
"Product": "Awesome T-Shirt",
"Price": "R200.00",
"Quantity": 2
},
{
"Product": "Delphi for Dummies (Book)",
"Price": "1000.00",
"Quantity": 1
}
]
}
}

You could do this:

var
Order: Variant;
OrderItems: PDocVariantData;
i: Integer;
begin
Order := _Json(InputStr).Order;
OrderItems := DocVariantData(Order.OrderItems);
for i := 0 to OrderItems.Count - 1 do
begin
Writeln(OrderItems.Values[i].Product);
Writeln(OrderItems.Values[i].Price);
Writeln(OrderItems.Values[i].Quantity);
end;
end.

To save a variant like this back to a JSON string, I have found I can simply call the VariantSaveJSON method.

Object serialization

The above method works well, but obviously I didn’t want to craft my JSON by hand each time. Thankfully, mORMot provides an ObjectToVariant method that converts a TObject to a variant such as the ones used above. This allows you to serialize all the published properties of the object by calling VariantSaveJSON.

It is recommended to derive your classes from TSynPersistent instead of the TPersistent class built into the VCL, e.g.

TBook = class(TSynPersistent)
private
FTitle: string;
FAuthor: string;
FPublished: TDateTime;
published
property Title: string read FTitle write FTitle;
property Author: string read FAuthor write FAuthor;
property Published: TDateTime read FPublished write FPublished;
end;

With this class, you can do this:

var
Book: TBook;
BookJSON: Variant;
begin
Book := TBook.Create;
try
Book.Title := 'Pig Island';
Book.Author := 'Mo Hayder';
Book.PublishDate := EncodeDate(2006, 4, 3);
BookJSON := ObjectToVariant(Book);
finally
Book.Free;
end;
  Writeln(VariantSaveJSON(BookJSON));
end.

Which outputs the following:

{"Title":"Pig Island","Author":"Mo Hayder","PublishDate":"2006-04-03"}

Note: While writing this article, I discovered that VariantSaveJSON does not detect my properties unless the Synopse.inc file is included in my project ( imported via mORMot.pas). Interesting.

There is an ObjectToJSON method as well, but I haven’t used it yet.

It is my understanding that in later versions of Delphi, records have run-time type information (RTTI) too. However, in Delphi 6, I had to either use objects as demonstrated above, or use text-based type definitions for my records (this topic is worth an article of its own).

Serializing collections of objects

The easiest way I have found to serialize collections of objects is through the use of object arrays. This uses, quite simply, an array type based on your own class, e.g.

TBookObjArray = array of TBook;

It is necessary to declare a type for this object array, and the reason for that is you have to register the array type with mORMot’s TJSONSerializer class before you can serialize it.

Now I can use this array type in another object, and serialize that object.

type
TLibrary = class(TSynPersistent)
private
FBooks: TBookObjArray;
published
property Books: TBookObjArray read FBooks write FBooks;
end;
var
MyLibrary: TLibrary;
BookArray: TBookObjArray;
LibraryJSON: Variant;
begin
TJSONSerializer.RegisterObjArrayForJSON([TypeInfo(TBookObjArray), TBook]);
  MyLibrary := TLibrary.Create;
try
SetLength(BookArray, 3);
BookArray[0] := TBook.Create;
BookArray[0].Title := 'The Quantum Thief';
BookArray[0].Author := 'Hannu Rajaniemi';
BookArray[1] := TBook.Create;
BookArray[1].Title := 'The Fractal Prince';
BookArray[1].Author := 'Hannu Rajaniemi';
BookArray[2] := TBook.Create;
BookArray[2].Title := 'The Causal Angel';
BookArray[2].Author := 'Hannu Rajaniemi';
    MyLibrary.Books := BookArray;
LibraryJSON := ObjectToVariant(MyLibrary);
finally
MyLibrary.Free;
end;
  Writeln(VariantSaveJSON(LibraryJSON));
end.

(Remember to free your book objects!)

This outputs the following:

{"Books":[{"Title":"The Quantum Thief","Author":"Hannu Rajaniemi","PublishDate":""},{"Title":"The Fractal Prince","Author":"Hannu Rajaniemi","PublishDate":""},{"Title":"The Causal Angel","Author":"Hannu Rajaniemi","PublishDate":""}]}

Because I’m using the TDocVariantData approach for deserialization, I haven’t yet played around with the JSONToObject method, but I expect it to work in much the same way.