Blog

All Blog Posts  |  Next Post  |  Previous Post

Diving deeper: JSON persistence, part 1/4: Basics

Bookmarks: 

Tuesday, August 2, 2022

TMS Software Delphi  Components

Intro

TMS FNC Core is a universal core layer for creating rich visual and non-visual components for VCL, FMX, LCL and WEB core apps. A major part of TMS FNC Core is the ability to save and load objects to and from JSON. There is a complete JSON parser, reader and writer included. Components inheriting from the base TTMSFNCCustomControl, TTMSFNCCustomComponent class and including the unit (FMX.)(VCL.)(WEBLib.)(LCL)TMSFNCPersistence.pas get JSON persistence by default. Even when only using TMS FNC Core, objects that are not directly related to FNC can be persisted with a couple of lines of code.

The "Diving Deeper: JSON persistence" blog series will consist out of four parts:

  1. Basic object persistence
  2. Collections
  3. Generic lists & dictionaries
  4. Undo / Redo Manager

The blog series will use a TPerson class and eventually lead into a class structure that has all the bits and pieces together required for JSON persistence. Note that when referring to unit names, the prefix of the framework ((FMX.)(VCL.)(WEBLib.)(LCL)) will not be included, for readability purposes. The code snippets are written in FMX as a default framework, but can easily be ported to other frameworks. If you have any questions during the blog series, don't hesitate to ask them in the comments section, or by using our support channels.

Units

After installing TMS FNC Core, the most important units are the TMSFNCPersistence.pas and the TMSFNCTypes.pas unit. There are a couple of class helper functions in the TMSFNCUtils unit, but that is of lesser importance and mostly revolve around parsing raw JSON content.

  • TMSFNCPersistence.pas: Unit containing interfaces and class functions to save and load JSON data to and from objects
  • TMSFNCTypes.pas: Unit containing some class helpers to assign JSON data, which internally map on the TMSFNCPersistence.pas unit.

TMSFNCPersistence.pas is a unit that contains 2 important classes, TTMSFNCObjectPersistence and TTMSFNCPersistence. TTMSFNCObjectPersistence has a class function to save and a class procedure to load JSON data to and from an object.

TTMSFNCObjectPersistence = class
public
  class function SaveObjectToString(AObject: TObject): string;
  class procedure LoadObjectFromString(AObject: TObject; AString: string);
end;

TTMSFNCPersistence is the base class that provides a lot of functionality to detect property names, types and the ability to read and write custom properties. In this series we will not go into much detail, but the details of this class might be covered in a future blog post. For the purposes of these blog series, the focus lies on the TTMSFNCObjectPersistence class, the TTMSFNCPersistence class basic load/save methods and the class helpers available in the TMSFNCTypes.pas unit.

Published properties

Before we get started, there is one important requirement to make sure that our object and its properties get persisted. Only published properties will be persisted. Internally, a cross-platform method is used and currently maps on published properties only due to technical reasons. This might expand to public properties as well in the future, but for now, it's important to move properties to the published section in order to persist them, the same way as is required when creating components that have properties persisted in the form file.

Basic object persistence: TPerson

Let's get started by defining our TPerson class

type
  TPersonAddress = class(TPersistent)
  private
    FPostalCode: string;
    FAddressLocality: string;
    FAddressRegion: string;
    FStreetAddress: string;
  published
    property AddressLocality: string read FAddressLocality write FAddressLocality;
    property AddressRegion: string read FAddressRegion write FAddressRegion;
    property PostalCode: string read FPostalCode write FPostalCode;
    property StreetAddress: string read FStreetAddress write FStreetAddress;
  end;

  TPerson = class(TPersistent)
  private
    FAddress: TPersonAddress;
    FColleagues: TStringList;
    FBirthDate: string;
    FName: string;
    FEmail: string;
    FTelephone: string;
    FGender: string;
    FNationality: string;
    FJobTitle: string;
    FURL: string;
  public
    constructor Create;
    destructor Destroy; override;
  published
    property Address: TPersonAddress read FAddress;
    property Colleagues: TStringList read FColleagues;
    property Email: string read FEmail write FEmail;
    property JobTitle: string read FJobTitle write FJobTitle;
    property Name: string read FName write FName;
    property BirthDate: string read FBirthDate write FBirthDate;
    property Gender: string read FGender write FGender;
    property Nationality: string read FNationality write FNationality;
    property Telephone: string read FTelephone write FTelephone;
    property URL: string read FURL write FURL;
  end;

As you can see, TPerson has a property Address which is again an object of type TPersonAddress, additionally it also contains a Colleague: TStringList property which will be mapped on a JSON array. Nested object properties are also handled out of the box.

Inspecting TPerson

When creating an instance of TPerson, there is a nice utility helper method that can log an object in JSON to the IDE debug output window. When including the TMSFNCTypes unit, call [Object].Log; to output the JSON as shown in the following sample.

var
  p: TPerson;
begin
  p := TPerson.Create;
  try
    p.Log;
  finally
    p.Free;
  end;
end;
The output of the object in JSON will be rendered in one line. To view it properly, there are a lot of online JSON "beautifiers" available that take care of parsing and rendering in a readable way. Rendering our object after initializing it, will show the JSON structure and also, that there is no info available as there are no properties set. Changing properties on our created TPerson object, will of course have impact on the output of our object.

{
  "$type": "TPerson",
  "Address": {
    "$type": "TPersonAddress",
    "AddressLocality": "",
    "AddressRegion": "",
    "PostalCode": "",
    "StreetAddress": ""
  },
  "BirthDate": "",
  "Colleagues": [],
  "Email": "",
  "Gender": "",
  "JobTitle": "",
  "Name": "",
  "Nationality": "",
  "Telephone": "",
  "URL": ""
}

Loading Data

For testing purposes, we have predefined a jsonSample constant which already contains JSON data ready to be loaded into the TPerson object.

const
  jsonSample =
    '{' +
      '"$type": "TPerson",' +
      '"address":{' +
        '"$type": "TPersonAddress",' +
        '"addressLocality":"Colorado Springs",' +
        '"addressRegion":"CO",' +
        '"postalCode":"80840",' +
        '"streetAddress":"100 Main Street"' +
      '},' +
      '"colleagues":[' +
        '"http://www.example.com/JohnColleague.html",' +
        '"http://www.example.com/JameColleague.html"' +
      '],' +
      '"email":"info@example.com",' +
      '"jobTitle":"Research Assistant",' +
      '"name":"Jane Doe",' +
      '"birthDate":"1979-10-12",' +
      '"gender":"female",' +
      '"nationality":"Albanian",' +
      '"telephone":"(123) 456-6789",' +
      '"url":"http://www.example.com"' +
    '}';

As explained there are multiple ways to map the JSON data onto the TPerson object. 

1. Use the TTMSFNCPersistence class

Add the unit *TMSFNCPersistence to the uses list (* = FMX., LCL, VCL., WEBLib.), and use the following code:

var
  p: TPerson;
  s: TStringStream;
begin
  p := TPerson.Create;
  s := TStringStream.Create(jsonSample);
  try
    TTMSFNCPersistence.LoadSettingsFromStream(p, s);
  finally
    s.Free;
    p.Free;
  end;
end;

2. Use the TTMSFNCObjectPersistence class

An alternative is to use the class TTMSFNCObjectPersistence that maps JSON to the object. (string variable or constant only)

var
  p: TPerson;
begin
  p := TPerson.Create;
  try
    TTMSFNCObjectPersistence.LoadObjectFromString(p, jsonSample);
  finally
    p.Free;
  end;
end;

3. Use the object class helper in *TMSFNCTypes unit

An alternative is to use the class helper that maps JSON to the object via a class helper. (string variable or constant only)

var
  p: TPerson;
begin
  p := TPerson.Create;
  try
    p.JSON := jsonSample;
  finally
    p.Free;
  end;
end;

Saving Data

Writing to JSON is as easy as reading. Simply use SaveSettingsToFile or SaveSettingsToStream or use the JSON object class helper to get the JSON from the object. For each of the above three load methods, there are an equivalent in save methods as well. For testing purposes, we want to change the name of "Jane Doe", to "Joe Heart" first to demonstrates how the result of the JSON output changes accordingly.

var
  p: TPerson;
begin
  p := TPerson.Create;
  try
    p.JSON := jsonSample;
    p.Name := 'Joe Heart';
    TTMSFNCUtils.Log(p.JSON);
    //or
    TTMSFNCPersistence.SaveSettingsToFile(p, 'TPerson.json');
  finally
    p.Free;
  end;
end;

Alternatively, the TTMSFNCObjectPersistence class can be used to save the object to a JSON string

var
  p: TPerson;
  j: string;
begin
  p := TPerson.Create;
  try
    j := TTMSFNCObjectPersistence.SaveObjectToString(p);
  finally
    p.Free;
  end;
end;

Or by using the TMSFNCTypes unit class helpers

var
  p: TPerson;
  j: string;
begin
  p := TPerson.Create;
  try
    j := p.ToJSON;
  finally
    p.Free;
  end;
end;

$type property

The above data loading expects the TPerson object to exist and be accessible. In some situations however, the object will not be available when loading the JSON data, or some situations might expect the object to be loaded alongside the JSON data. As you might already have noticed, in the JSON output of the object, there are properties named "$type", which map on the classes of the objects in JSON. These properties are used for the JSON persistence in FNC to be able to instantiate those classes. To make sure the object(s) can be created when required, it's good practice to register your classes. In case of our TPerson class, this translates to:

RegisterClass(TPerson);
RegisterClass(TPersonAddress);

Especially in case of collections and generics, the classname of the objects are used, but this is covered in the next part of this blog series, where we are going to handle collections.

Feedback

This blog content might sound familiar and indeed, it is! Some while ago we wrote this blog post, which basically almost covers the first part and we wanted to start of with something familiar. We had a series of questions afterwards and thought about relaunching the FNC JSON persistence in a 4 part blog series. Next up will be how to handle collections, so stay tuned for more to come! As always, please leave a comment or if you have any questions, don't hesitate to ask us!



Pieter Scheldeman


Bookmarks: 

This blog post has not received any comments yet.



Add a new comment

You will receive a confirmation mail with a link to validate your comment, please use a valid email address.
All fields are required.



All Blog Posts  |  Next Post  |  Previous Post