Serializing Generic Object Lists with TJson
Uwe Raabe - 02/Mar/2020
Uwe Raabe - 02/Mar/2020
[SHOWTOGROUPS=4,20]
In the previous blog post (<-- click here) we learned how to decorate a field of a class with attributes to adjust the Json serialization to our needs. This post is about serializing fields of type TObjectList<T> or descendants thereof.
Let’s recap the problem and the final question: The Json serializer does a good job for array of objects, but fails miserably on TObjectList<T> fields.
What can we do to make serializing generic object lists being serialized properly in both directions?
As seen in the previous post, our best bet will be some neat attributes to decorate the fields with. The JsonUTCDate attribute served well, so what about a JsonObjectList attribute registering some fancy interceptor handling the nitty-gritty details. As you might have guessed, this time it is not that easy.
To populate a generic object list, the serializer must create the individual objects. For that it needs to know the type of these objects. For arrays that is already handled inside TJSONUnMarshal.JSONToTValue by asking RTTI for the ElementType.
For a generic object list we must provide that type, so we declare a generic TObjectListInterceptor:
The implementation doesn’t look that complicated, but I want to make clear that we never set the field value! We only use GetValue to get hands on the actual list instance. This has some consequences later, so we better remember it.
The JsonObjectList attribute takes the actual interceptor class as a parameter and registers it a s ctObjects converter and rtObjects reverter:
Let me emphasize the difference of a rtObjects and a rtTypeObjects reverter. The corresponding methods have the following signatures:
You can see that ObjectsReverter is meant to handle the Args list (which is basically an array of TObject) by doing whatever is necessary to the Field of the Data object.
Meanwhile the TypeObjectsReverter must return an instance built from the Args list, which is going to replace any existing instance present in the object field. For this to work, we need the actual type of the list – in addition to the element type.
The cast to TObjectList<T> seen above works simply because we are acting on the actual field instance with the correct type and it just happens that the called methods Count, Clear and Add wire directly on the FListHelper instance unaffected of the actual type <T>.
The choose of a rtObjects reverter requires the element type as the only generic type parameter. I will later explain why a derived TObjectList<T> cannot be decorated with a rtTypeObjects attribute, ruling out such an approach.
Now we want to make use of the new attribute and see how it works for a generic object list field of our class.
Unfortunately that doesn’t even compile!
It turned out that the compiler is not able to resolve the instantiated generic type TObjectListListInterceptor<TContact> used as a parameter for the JsonObjectList attribute. We have to declare an alias for that to make it work.
Note that we need at least one type keyword between the alias and its use inside the attribute.
Are we finished now?
Ehm, no! Perhaps you remember me saying “I want to make clear that we never set the field value!”. That sentence assumes that there already is an instance of TContactList present in the FContacts field.
No problem, we can create that instance in the constructor and free it in the destructor. We probably would have done that anyway. Unfortunately that isn’t enough. The standard implementation replaces all instances in the fields with nil before doing any de-serialization of an object and frees those saved field instances after the field gets a new valid instance created in between –
– unless we tell not to do so.
The attribute for skipping the destruction of the field instances is JSONOwned with parameter False. The final attribute decoration for a generic object list field now looks like this:
There we are! Now we are able to use generic object lists as fields while still serializing them as object arrays and vice versa. The additional declaration of an interceptor alias and the per field decoration with some attributes seems acceptable given this advantage.
Why no attribute directly for TContactList?
You remember that the TypeObjectsReverter needs to know the actual list class. A possible declaration might be TObjectListInterceptor<T: class, ListT: TObjectList<T>>, which is indeed possible to implement. The problem is the use of the attribute which would have to be like this:
Unfortunately this doesn’t compile, because TContactListInterceptor is not fully defined when used in the attribute. We need to fully define the interceptor class using TContactList before we can decorate TContactList with that attribute. If anyone comes up with a workaround, tell me.
The source code used in this article can be found at Для просмотра ссылки Войдиили Зарегистрируйся
[/SHOWTOGROUPS]
In the previous blog post (<-- click here) we learned how to decorate a field of a class with attributes to adjust the Json serialization to our needs. This post is about serializing fields of type TObjectList<T> or descendants thereof.
Let’s recap the problem and the final question: The Json serializer does a good job for array of objects, but fails miserably on TObjectList<T> fields.
What can we do to make serializing generic object lists being serialized properly in both directions?
As seen in the previous post, our best bet will be some neat attributes to decorate the fields with. The JsonUTCDate attribute served well, so what about a JsonObjectList attribute registering some fancy interceptor handling the nitty-gritty details. As you might have guessed, this time it is not that easy.
To populate a generic object list, the serializer must create the individual objects. For that it needs to know the type of these objects. For arrays that is already handled inside TJSONUnMarshal.JSONToTValue by asking RTTI for the ElementType.
For a generic object list we must provide that type, so we declare a generic TObjectListInterceptor:
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 | type TObjectListInterceptor<T: class> = class(TJSONInterceptor) public procedure AfterConstruction; override; function ObjectsConverter(Data: TObject; Field: string): TListOfObjects; override; procedure ObjectsReverter(Data: TObject; Field: string; Args: TListOfObjects); override; end; implementation procedure TObjectListInterceptor<T>.AfterConstruction; begin inherited; ObjectType := T; end; function TObjectListInterceptor<T>.ObjectsConverter(Data: TObject; Field: string): TListOfObjects; var I: Integer; ctx: TRTTIContext; list: TObjectList<T>; begin list := TObjectList<T>(ctx.GetType(Data.ClassType).GetField(Field).GetValue(Data).AsObject); SetLength(Result, list.Count); for I := 0 to list.Count - 1 do Result := list.Items; end; procedure TObjectListInterceptor<T>.ObjectsReverter(Data: TObject; Field: string; Args: TListOfObjects); var ctx: TRTTIContext; list: TObjectList<T>; obj: TObject; begin list := TObjectList<T>(ctx.GetType(Data.ClassType).GetField(Field).GetValue(Data).AsObject); list.Clear; for obj in Args do list.Add(T(obj)); end; |
The implementation doesn’t look that complicated, but I want to make clear that we never set the field value! We only use GetValue to get hands on the actual list instance. This has some consequences later, so we better remember it.
The JsonObjectList attribute takes the actual interceptor class as a parameter and registers it a s ctObjects converter and rtObjects reverter:
1 2 3 4 5 6 7 8 9 10 11 12 | type JsonObjectListAttribute = class(JsonReflectAttribute) public constructor Create(InterceptorType: TClass); end; implementation constructor JsonObjectListAttribute.Create(InterceptorType: TClass); begin inherited Create(ctObjects, rtObjects, InterceptorType); end; |
Let me emphasize the difference of a rtObjects and a rtTypeObjects reverter. The corresponding methods have the following signatures:
1 2 | procedure ObjectsReverter(Data: TObject; Field: string; Args: TListOfObjects); function TypeObjectsReverter(Data: TListOfObjects): TObject; |
You can see that ObjectsReverter is meant to handle the Args list (which is basically an array of TObject) by doing whatever is necessary to the Field of the Data object.
Meanwhile the TypeObjectsReverter must return an instance built from the Args list, which is going to replace any existing instance present in the object field. For this to work, we need the actual type of the list – in addition to the element type.
The cast to TObjectList<T> seen above works simply because we are acting on the actual field instance with the correct type and it just happens that the called methods Count, Clear and Add wire directly on the FListHelper instance unaffected of the actual type <T>.
The choose of a rtObjects reverter requires the element type as the only generic type parameter. I will later explain why a derived TObjectList<T> cannot be decorated with a rtTypeObjects attribute, ruling out such an approach.
Now we want to make use of the new attribute and see how it works for a generic object list field of our class.
1 2 3 4 5 | type TMyAddressBook = class private [JsonObjectList(TObjectListListInterceptor<TContact>)] FContacts: TObjectList<TContact>; |
Unfortunately that doesn’t even compile!
It turned out that the compiler is not able to resolve the instantiated generic type TObjectListListInterceptor<TContact> used as a parameter for the JsonObjectList attribute. We have to declare an alias for that to make it work.
1 2 3 4 5 6 7 8 9 10 | type TContactList = TIdentList<TContact>; TContactListInterceptor = TObjectListInterceptor<TContact>; type TMyAddressBook = class private [JsonObjectList(TContactListInterceptor)] FContacts: TContactList; ... |
Note that we need at least one type keyword between the alias and its use inside the attribute.
Are we finished now?
Ehm, no! Perhaps you remember me saying “I want to make clear that we never set the field value!”. That sentence assumes that there already is an instance of TContactList present in the FContacts field.
No problem, we can create that instance in the constructor and free it in the destructor. We probably would have done that anyway. Unfortunately that isn’t enough. The standard implementation replaces all instances in the fields with nil before doing any de-serialization of an object and frees those saved field instances after the field gets a new valid instance created in between –
– unless we tell not to do so.
The attribute for skipping the destruction of the field instances is JSONOwned with parameter False. The final attribute decoration for a generic object list field now looks like this:
1 2 3 4 5 6 | type TMyAddressBook = class private [JSONOwned(False), JsonObjectList(TContactListInterceptor)] FContacts: TContactList; ... |
There we are! Now we are able to use generic object lists as fields while still serializing them as object arrays and vice versa. The additional declaration of an interceptor alias and the per field decoration with some attributes seems acceptable given this advantage.
Why no attribute directly for TContactList?
You remember that the TypeObjectsReverter needs to know the actual list class. A possible declaration might be TObjectListInterceptor<T: class, ListT: TObjectList<T>>, which is indeed possible to implement. The problem is the use of the attribute which would have to be like this:
1 2 3 4 5 6 7 | type TContactListInterceptor = class; [JsonObjectList(TContactListInterceptor)] TContactList = TObjectList<TContact>; TContactListInterceptor = TObjectListInterceptor<TContact, TContactList>; |
Unfortunately this doesn’t compile, because TContactListInterceptor is not fully defined when used in the attribute. We need to fully define the interceptor class using TContactList before we can decorate TContactList with that attribute. If anyone comes up with a workaround, tell me.
The source code used in this article can be found at Для просмотра ссылки Войди
[/SHOWTOGROUPS]