Articles Serializing Objects with TJson by Uwe Raabe

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,439
Credits
574
Serializing Objects with TJson
Uwe Raabe - 02/Mar/2020
[SHOWTOGROUPS=4,20]
Serializing objects to Json as well as de-serializing them with the Delphi standard libraries has been subject to many discussions.

While the majority suggests to use another library or a self implemented solution, there are others who would prefer the built-in tools for a couple of reasons.

Simplicity and the availability with every (decent) Delphi installation being the most mentioned ones.

The ease and elegance of a TRESTRequest.AddBody<T>(myInstance) call is hard to attain with other means. I guess it is not myself alone being tempted to make use of it. With a bit of care taken when designing the objects to serialize the results are often quite satisfying and fit the requirements. Nourishing this with some advanced techniques shown in this post may be enough to keep the benefits without the need for external code.

If we inspect the above mentioned generic AddBody method we soon find out that the serializing part is outsourced to the TJson.ObjectToJsonObject method. We make a note that the call just accepts the default TJsonOptions for the second parameter.

Without any tweaking the Json serializer takes each field of the class, where it does a pretty good job for simple (value) types, objects and arrays of both. In reality the use of object arrays as fields inside a class is rarely a valid approach. Generic object lists derived from TObjectList<T> are way more feasible and thus the preferred way to go. Unfortunately those are not handled pretty well and in most cases are they even not what a non Delphi counterparts accept or deliver – mostly they use arrays for these lists. The ultimate question now is:

What can we do to make serializing generic object lists being serialized properly in both directions?
I hate to disappoint you, but the generic object lists will be covered in the next blog post (<-- click here) while this one tackles an easier task – it would have been too long otherwise.

Although the Json serializing is quite resistant to extensions, there are means to achieve more than appears possible on a first look. The allies in this endeavor are attributes.

Let’s start with a simple task as a warm-up. As mentioned above, the AddBody method uses the default TJsonOptions for the ObjectToJsonObject call, which are joDateIsUTC and joDateFormatISO8601. While the date format is not a bad choice, the requirements to provide UTC dates may force some work unwanted on us.

What if we could just say: Hey, this is a TDateTime field, but I want it to be converted to UTC in the Json representation.

In addition there should be a null date equivalent to the ISO string “0000-00-00T00:00:00.000Z”, but the Json for that shall be a null value for these instead of that string value.


To abbreviate this information we want an attribute named JsonUTCDate, so that a field declaration should look like this:

1
2
3
4
[JsonUTCDate]
FStartDate: TDateTime;
[JsonUTCDate]
FEndDate: TDateTime;

OK, this is the new attribute declaration and its implementation:

1
2
3
4
5
6
7
8
9
10
11
12
type
JsonUTCDateAttribute = class(JsonReflectAttribute)
public
constructor Create;
end;

implementation

constructor JsonUTCDateAttribute.Create;
begin
inherited Create(ctObject, rtString, TUTCDateTimeInterceptor);
end;

Pretty simple – because we delegate the actual work to a class named TUTCDateTimeInterceptor, which we will have a look at later. For the moment we notice that the JsonUTCDate attribute registers different types for the converter and the reverter. The interceptor acts as a ctObject converter, but as a rtString reverter. While we could accomplish most of the task also with a ctString converter, the demand for the null value requires us to exchange the Json string value with a Json null value. A ctString converter cannot be used for that.

The TUTCDateTimeInterceptor is derived from TJSONInterceptor and overrides the methods ObjectConverter and StringReverter according to the values given in JsonUTCDateAttribute.Create. The implementation for StringReverter is pretty straight forward based on the original implementation found in TISODateTimeInterceptor decorated with a call to a small function ToLocalTime, which takes care of the null date value (We don’t want the null date being converted to local time, do we?):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const
cNoDate = -DateDelta;

function IsNoDate(ADate: TDateTime): Boolean;
begin
Result := Round(ADate) = cNoDate;
end;
function TUTCDateTimeInterceptor.ToLocalTime(const ADateTime: TDateTime): TDateTime;
begin
Result := ADateTime;
if not IsNodate(Result) then
Result := TTimeZone.Local.ToLocalTime(Result);
end;

procedure TUTCDateTimeInterceptor.StringReverter(Data: TObject; Field, Arg: string);
var
ctx: TRTTIContext;
datetime: TDateTime;
begin
datetime := ToLocalTime(ISO8601ToDate(Arg));
ctx.GetType(Data.ClassType).GetField(Field).SetValue(Data, datetime);
end;

The ObjectConverter method is a bit more sophisticated to achieve the null value requirement:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function TUTCDateTimeInterceptor.ToUniversalTime(const ADateTime: TDateTime; const ForceDaylight: Boolean): TDateTime;
begin
Result := ADateTime;
if not IsNoDate(Result) then
Result := TTimeZone.Local.ToUniversalTime(Result, ForceDaylight);
end;

function TUTCDateTimeInterceptor.ObjectConverter(Data: TObject; Field: string): TObject;
var
ctx: TRTTIContext;
date: TDateTime;
begin
Result := nil;
date := ctx.GetType(Data.ClassType).GetField(Field).GetValue(Data).AsType<TDateTime>;
if IsNoDate(date) then Exit;
StringProxy.Value := DateToISO8601(ToUniversalTime(date));
result := StringProxy;
end;

First it takes the current date value from the field and checks for the null date, exiting with a nil result if found. A nil object will end up as a null in the Json string.

For all other dates we convert to UTC with the ToUniversalTime method. Alas, we cannot return the resulting string directly – we need to return an object. For this we use a local instance of a TStringProxy object, for which we have taken ownership to avoid memory leaks. The TStringProxy class is decorated with ctTypeString interceptor to just retrieve the plain string. If we omit that we get an inner object containing the value as a separate field instead of just the value for the current TDateTime field.

This is the complete class declaration for TUTCDateTimeInterceptor together with the missing methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
type
TUTCDateTimeInterceptor = class(TJSONInterceptor)
private type
TStringProxyInterceptor = class(TJSONInterceptor)
public
function TypeStringConverter(Data: TObject): string; override;
end;
[JsonReflect(ctTypeString, rtTypeString, TStringProxyInterceptor)]
TStringProxy = class
private
FValue: string;
public
property Value: string read FValue write FValue;
end;
var
FStringProxy: TStringProxy;
function GetStringProxy: TStringProxy;
strict protected
function ToLocalTime(const ADateTime: TDateTime): TDateTime;
function ToUniversalTime(const ADateTime: TDateTime; const ForceDaylight: Boolean = False): TDateTime;
property StringProxy: TStringProxy read GetStringProxy;
public
destructor Destroy; override;
procedure StringReverter(Data: TObject; Field: string; Arg: string); override;
function ObjectConverter(Data: TObject; Field: string): TObject; override;
end;

function TUTCDateTimeInterceptor.GetStringProxy: TStringProxy;
begin
if FStringProxy = nil then begin
FStringProxy := TStringProxy.Create;
end;
Result := FStringProxy;
end;

destructor TUTCDateTimeInterceptor.Destroy;
begin
FStringProxy.Free;
inherited Destroy;
end;

That’s it for now.

The code show in this article can be found on GitHub: Для просмотра ссылки Войди или Зарегистрируйся

[/SHOWTOGROUPS]
 
Последнее редактирование: