Convert an object instance into a JSON string and making use of custom attributes

Delphi has become very flexible when it comes to handling JSON data. However, as I had to find out myself today: to get to know about this flexibility is a chore. First of all, the documentation never tells us to look in REST.Json  for all the neat stuff instead of System.JSON .

The beforementioned unit offers the class TJSON that makes converting object instances into a JSON string a one-line-task. However, before getting into an example, be aware that not only the documentation lacks the proper references, to make things worse, you may read the comment in System.JSON right at the beginning:

unit System.JSON;

/// <summary>
/// System.JSON implements a TJson class that offers several convenience methods:
/// - converting Objects to Json and vice versa
/// - formating Json  </summary>

interface

Try looking for it. It is not there.  And – for the record – the typo (‘formating’) is not mine either… The naming inconsistency with ‘JSON’ and ‘Json’ is also copied from the Delphi sources.

You will find it in REST.Json instead. The new System.JSON offers other great classes to make things easier like using an XPath-like syntax to navigate JSON data.

So, looking at REST.Json we find the mentioned class TJson:

TJson = class(TObject)

  public
    
    class function ObjectToJsonObject(AObject: TObject; AOptions: TJsonOptions = [joDateIsUTC, joDateFormatISO8601]): TJSOnObject;   
    class function ObjectToJsonString(AObject: TObject; AOptions: TJsonOptions = [joDateIsUTC, joDateFormatISO8601]): string;
    class function JsonToObject<T: class, constructor>(AJsonObject: TJSOnObject; AOptions: TJsonOptions = [joDateIsUTC, joDateFormatISO8601]): T; overload;
    class function JsonToObject<T: class, constructor>(AJson: string; AOptions: TJsonOptions = [joDateIsUTC, joDateFormatISO8601]): T; overload;
    class procedure JsonToObject(AObject:TObject; AJsonObject: TJSOnObject; AOptions: TJsonOptions = [joDateIsUTC, joDateFormatISO8601]); overload;
    
  end;

The listing above does not include all of the methods.

In order to convert an object instance to a JSON string we will thus use ‘TJson.ObjectToJsonString’. The method is defined as a ‘class method’, which means that you do not need to an object in order to call this method. You instead use the class name and the method name to invoke it.

type
  TFoo = class
    private
      NamePriv: String;

    public
      NamePub: String;
  end;

Above is the definition of my very simple test class. It contains two fields, one declared in the private and one in the public section. Will it make a difference?

procedure TForm1.Button1Click(Sender: TObject);
var
  lFoo : TFoo;

begin
  lFoo := TFoo.Create;
  lFoo.NamePriv := 'private Name';
  lFoo.NamePub := 'public Name';
  ShowMessage( TJson.ObjectToJsonString(lFoo) );
  lFoo.Free;
end;

The message will be:

{"namePriv":"private Name","namePub":"public Name"}

Who would have thought…. you will not find this information in the  documentation. There is also one more tid-bit to remember: If you start your field name with a ‘F’ as it is common habit in Delphi, the  leading ‘F’ will be omitted from the name.

Furthermore, only fields will be included in the JSON string. Properties will be ignored, which makes sense as properties are a way to access fields of a class.

Still, right now, we are unable to exclude some fields from the JSON string and we are also a bit restricted when it comes to naming the elements inside the JSON string.

Custom attributes to the rescue!

The Delphi language supports so-called custom attributes. Do not ask me when this feature was included. I think it was Delphi 2010. Custom attributes allow you to mark elements of a class with certain “markers”. And to make things even more flexible, you can add parameters to these markers.

This makes it possible to exclude fields from the JSON string and also name elements.

For this example we will use 2 custom attributes:

 /// <summary>Attribute that specifies whether a field or type should be
  /// marshalled/unmarshalled. If the attribute is not present, defaults to true.
  /// If false, the field/type will be skipped during the marshalling and
  /// unmarshalling process</summary>
  JSONMarshalledAttribute = class(JSONBooleanAttribute)
  end;

  JSONNameAttribute = class(TCustomAttribute)
  private
    FName: string;
  public
    constructor Create(AName: string);
    property Name: String read FName;
  end;

Sadly, the source code lacks the descripion for the JSONName Attribute in its comments. JSONName allows you to use a different name for a field in the JSON string.

We can use the two attributes as follows:

type
  TFoo = class
    private
      [JsonMarshalled(false)]
      NamePriv: String;

    public
      [JsonName('Name')]
      NamePub: String;
  end;

This will tell ObjectToJsonString not to include NamePriv and to name NamePub as ‘Name’ in the JSON string. The resulting JSON looks as follows:

{"Name":"public Name"}

Exactly what we wanted.

Custom attributes are pretty neat in this case as it allows for the JSON definition to be done in the class declaration and the ‘user’ of the class does not need to pay any attention to this fact.

If you try to reproduce this code you might hit a major obstacle. In my case Delphi complained with the following compiler warning and my custom attribute definitions were always ignored:

 [dcc32 Warning] Unit1.pas(20): W1025 Unsupported language feature: ‘custom attribute’

The documentation does not give any hints whatsoever. Sadly, the internet is filled with an huge amount of misinformation on this error message. I admit that I tried for hours solving this problem. Thankfully, I have Robert Love as a Facebook buddy and I nagged him about the error message in the afternoon (his morning).

He explained that the error message is rather poorly phrased. Delphi is telling us that the custom attribute class cannot be resolved by the compiler, i.e. the definition for JsonNameAttribute and JsonMarshallAttribute. Note that the  usual ‘T’ prefix is missing.

Thus, simply pay attention in your uses-clause to include REST.JSON.Types  before you use the custom attributes and it will all work like a charm. I was also unable to find any mention of JsonName and JsonMarshalled in the explanation of ObjectToJsonString.

Thus, I think this blog post might come in handy for somebody trying to get started with JSON without using the ClientDataset adapters, provided by the new REST client classes. (Thanks, Rob!)

Summed up it has to be said that the handling of JSON with Delphi has become very easy over the years. However, it is tough to get started with all the flexibility out there.

Tags: , ,
3 comments on “Convert an object instance into a JSON string and making use of custom attributes
  1. Very good investigation, Holger. We had the solution ready and we don’t knew the existence. You saved me a lot of work to navigate into the “JSON sea”. The DBXJSONReflect library generate spurious JSON string, that do not correctly decoded by other languages like Net framework, while REST.Json yes!!
    Thank you.

  2. Holger Casties says:

    Hi Holger.
    It is not a reply on your blog rather a relating question.

    I tried the following:

    type
    TGuidInterceptor = class(TJSONInterceptor)
    public
    function StringConverter(Data: TObject; Field: string): string; override;
    procedure StringReverter(Data: TObject; Field: string; Arg: string); override;
    end;

    TFoo = class
    private
    fName: String;
    [JsonReflect(ctString, rtString, TGuidInterceptor)]
    fGuids: TDictionary;
    public
    constructor Create(const name: String);
    destructor Destroy; override;

    property name: String read fName write fName;
    property guids: TDictionary read fGuids write fGuids;
    end;

    When using ‘fooJson := TJson.ObjectToJsonString(foo);’ the interceptor method ‘StringConverter()’ is invoked.
    But when using ‘TJson.JsonToObject(fooJson);’ the interceptor method ‘StringReverter()’ is NOT invoked.

    Do you have an explanation for that?

    It works when I just use TGUID instead of TDictionay!

  3. Holger Casties says:

    KEY of TDictionary is a String and VALUE is TGUID!

1 Pings/Trackbacks for "Convert an object instance into a JSON string and making use of custom attributes"
  1. […] last blog post (Link) covered the topic of converting object instances into a JSON string. The rather common case needs […]

Partnerships




Top