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.
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.
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!
KEY of TDictionary is a String and VALUE is TGUID!