Delphi Superobject, generic list to json - json

I have a object with some TObjectList<>-fields that I try to encode as JSON with help form SuperObject.
TLogs = TObjectList<TLog>;
TMyObject = class(TObject)
private
FLogs: TLogs;
end;
Deep inside SuperObjects code, there is a ToClass procedure, iterating the fields and add them to the json result.
In this loop, there is a check on the TRttiFields FieldType. If it's nil, it skips the object.
for f in Context.GetType(Value.AsObject.ClassType).GetFields do
if f.FieldType <> nil then
begin
v := f.GetValue(value.AsObject);
result.AsObject[GetFieldName(f)] := ToJson(v, index);
end
My generic list fields have a FieldType of nil. Why?
How can I make SuperObject serialize my list of objects?

This is a known issue in Delphi's RTTI creation. If you declare your generic class like that, it won't work. You need to use the class keyword.
TLogs = class(TObjectList<TLog>);
Hopefully this will be fixed in the next release.

Related

Default values for unknown elements in mORMot / Synopse JSON

I really love the way mORMot / Synopse has implemented the handling of JSON, especially the way you can use the JSON elements in your code (like MyString := myjson.name). This is very intuitive and useful in wrapping objects which only have one Variant (JSON) with state which we access through getters/setters, like below:
TMyObject = class
private
FState: Variant;
function GetName: String;
public
constructor Create(AJson: Variant);
property Name: String read GetName;
end;
function TMyObject.GetName: String;
begin
Result := FState.name;
end;
It is really powerful, but I would like to get the 'default' variant value if an element is not found in the corresponding JSON document (so, above example should return an empty string if 'name' is not present).
I don't want to use NullStrictConvert, because that is not thread safe and influences the rest of our program. Of course I could check VarIsNull(FState.name), but then I have to do this for each element and I prefer not to have this extra boilerplate.
Any suggestions?
When you unserialize some JSON into an object, the missing fields are left untouched IIRC.
So you can just set the fields to their default values before unserializing the JSON.
One way of doing it is to inherit from TSynPersistent and override the Create constructor and set default value(s).
Edit: You may use a TDocVariantData instead of variant, and call GetAsRawUTF8() and such methods which return false if the property is not existing.

Delphi REST.JSON TJson.JsonToObject Problem with inheritance

I would like to use the REST.JSON Librairy to convert an Object to a JSON-String and back, but i'm encountering some problems.
Let's say the classes i would like to convert is "TFooChild1" and "TFooChild2" which both descend from "TFoo".
The classes look like this:
TFoo = class
protected
Name: string;
Value: Double;
end;
TFooChild1 = class(TFoo)
private
Limit: Double;
end;
TFooChild2 = class(TFoo)
private
Limit: Double;
WorkerID: Integer;
end;
Creating and converting to JSON would look something like this:
var
Foo: TFoo;
s: string;
begin
Foo := TFooChild1.Create;
Foo.Name:= '...';
... //assign all the Fields
s := TJson.ObjectToJsonString(Foo);
Foo.Free;
//conversion to string is correct...
Foo := TJson.JsonToObject<TFoo>(s, []);
//Field "Limit" is lost
end
I'm aware that this (TJson.JsonToObject<TFoo>(s, [])) would never return the type TFooChild1, but that's exactly what i need.
When you convert back to an Object, the Fields of the child classes are lost. How do I avoid this? I can't do JsonToObject<TFooChild1> because I don't know if it's gonna be Child1 or Child2.
Any hints?
I've googled if there's maybe a ConvertOption that would include Type information, but i haven't found anything
Add Rest.JsonReflect to your uses class as you will need to use the [JsonReflect(ctObject,ctObject,TFooInterceptor)] attribute attached to where you are referencing the TFoo class. Then you will have to code a TFooInterceptor object to intercept and handle the appropriate conversion. I would code the ObjectReverter and ObjectConverter overrides and start coding from there. Your TFooInterceptor should descend from TJSONInterceptor. Some examples are in the source REST.Json.Interceptors.pas.

Serialize and deserialize a collection to JSON Delphi

I've searched a lot, but cannot find a working solution. Most likely because I'm pretty new to Delphi (and coming from C#).
I have a unit with the following class declaration:
TReplaceField = class
public
Key: string;
Value: string;
constructor Create; overload;
constructor Create(strKey, strValue: string); overload;
end;
Class can also be record, like suggested in the comment.
I want to create a collection of it, be it an array, TList<T>, or whatever suits best in this case, and then serialize it to JSON (to store it) and later deserialze it to loop over the fields.
I've found REST.JSON and TJson.ObjectToJsonString(), which works good for 1 object.
But if I pass an array of TReplaceField, it gives me a compile error:
E2010 Incompatible types: 'TObject' and 'Dynamic array'
When I use a TList at runtime, I get a different error:
type tkPointer is not currently supported
In C#, all of this is pretty easy, like a few lines of code. I'm pretty stuck on how to do it in Delphi (besides creating JSON by hand via string concatenation).
UPDATE:
With the tips from David, I've got the serialization working. Also deserialization of one object works, but not deserialization of a collection.
My class now looks like this:
TReplaceField = class
private
FKey : string;
FValue : string;
published
property Key : string read FKey write FKey;
property Value : string read FValue write FValue;
public
constructor Create; overload;
constructor Create(strKey, strValue: string); overload;
end;
Serialize one object: mmoJson.Lines.Text := TJson.ObjectToJsonString(Field); OK
Deserialize one object: JsonToObject<TReplaceField>(mmoJson.Lines.Text); OK
Serialize collection: mmoJson.Lines.Text := TJson.ObjectToJsonString(FieldList); OK
Deserialize collection: JsonToObject<TReplaceField>(mmoJson.Lines.Text); FAIL, message:
EConversionError with message 'Internal: Cannot instantiate type System.Generics.Collections.TList<uReplaceField.TReplaceFieldRec>'
Using a record instead of class for ReplaceField gives the same results.
Almost there. What am I missing to get it fully working?
type
TReplaceCollect = TList<TReplaceField>;
Add this after-class declaration.

How do I output the contents of a Set in Pascal?

I'm working with simple sets in Pascal, and simply want to output the contents of a set. Everytime I run the code i get the following error message: 'project1.lpr(17,13) Error: Can't read or write variables of this type'.
here is my code:
program Project1;
{$mode objfpc}{$H+}
uses
sysutils;
type TFriends = (Anne,Bob,Claire,Derek,Edgar,Francy);
type TFriendGroup = Set of TFriends;
Var set1,set2,set3,set4:TFriendGroup; x:integer;
begin
set1:=[Anne,Bob,Claire];
set2:=[Claire,Derek];
set3:=[Derek,Edgar,Francy];
writeln(set1);
readln;
end.
Is there a special method/function to output sets?
thanks
Free Pascal allows write/writeln() of enums without explicit typinfo calls.
So
{$mode objfpc} // or Delphi, For..in needs Object Pascal dialect iirc.
var Person :TFriends;
for Person in Set1 do
writeln(Person);
works fine.
Using WriteStr this can also be written to strings. (writestr functions like write/writestr but then to an string. Originally implemented for ISO/Mac dialects)
You can't directly display the set as a string because there is no type information emitted for it. To do so, your set must be a published property of a class.
Once published in a class, you can use the unit TypInfo to display the set as a string, using the function SetToString(). TypInfo is the FPC unit which does all the compiler reflection things.
Short working example of what you try to do:
program Project1;
{$mode objfpc}{$H+}
uses
sysutils, typinfo;
type
TFriends = (Anne,Bob,Claire,Derek,Edgar,Francy);
TFriendGroup = Set of TFriends;
TFoo = class
private
fFriends: TFriendGroup;
published
property Friends: TFriendGroup read fFriends write fFriends;
end;
Var
Foo: TFoo;
FriendsAsString: string;
Infs: PTypeInfo;
begin
Foo := TFoo.Create;
Foo.Friends := [Derek, Edgar, Francy];
//
Infs := TypeInfo(Foo.Friends);
FriendsAsString := SetToString(Infs, LongInt(Foo.Friends), true);
//
Foo.Free;
writeln(FriendsAsString);
readln;
end.
This program outputs:
[Derek,Edgar,Francy]
To go further:
official documentation about SetToString
this blog article

SuperObject Serializes Private Variables instead of Properties

I have the following code that serializes a dynamic array of classes. For some reason SuperObject serializes on the private variables instead of the class property names. Can anyone please advise how to fix this behaviour in SuperObject?
class function TJSON.AsJSON<T>(AObject: T; Indent: Boolean = False): string;
var
Ctx: TSuperRttiContext;
begin
Ctx := TSuperRttiContext.Create;
try
Result := Ctx.AsJson<T>(AObject).AsJSon(Indent);
finally
Ctx.Free;
end;
end;
type
TMyClass = class
private
FName_: String;
FAge_: Integer;
public
property Name: String read FName_ write FName_;
property Age: Integer read FAge_ write FAge_;
end;
procedure TFormTest.Button27Click(Sender: TObject);
var
MyClassArray: TArray<TMyClass>;
MyClass1, MyClass2: TMyClass;
begin
MyClass1 := TMyClass.Create;
MyClass1.Name := 'Joe';
MyClass1.Age := 10;
MyClass2 := TMyClass.Create;
MyClass2.Name := 'Dan';
MyClass2.Age := 13;
SetLength(MyClassArray, 2);
MyClassArray[0] := MyClass1;
MyClassArray[1] := MyClass2;
Memo1.Text := TJSON.AsJSON<TArray<TMyClass>>(MyClassArray);
end;
The above code generates the following JSON:
[{"FName_":"Joe","FAge_":10},{"FName_":"Dan","FAge_":13}]
what I am after is the following JSON:
[{"Name":"Joe","Age":10},{"Name":"Dan","Age":13}]
I think it's not possible at this time and that you probably hit this issue. Even Delphi XE2 Datasnap serializes private fields at JSON marshalling and in my view it's just a consequence of a deeper visibility given to the new extended RTTI without considering the limits.
As far as I know, RTTI operates only on published properties (I may be wrong), but I think that you should simply switch your properties access level to published to get the desired JSON string.