I need to custom marshal/unmarchal a TDictionary in Delphi (XE). The dictionary is declared as:
TMyRecord = record
key11: integer;
key12: string;
...
end;
TMyDict: TDictionary<string, TMyRecord>;
Now, if i marshal the dictionary without registering a custom converter, the marshaller will put all kind of fields in the JSON string - FOnValueNotify, FKeyCollection, FItems, etc.
What i need is some sort of associative array of associative arrays, i.e.
{"key1":{"key11":"val1","key12":"val2"},"key2":{"key11":"val3","key12":"val4"}}
Unfortunately, i don't know how to write the custom converter and reverter. I'm using Delphi XE and the built in TJSONMarshal and TJSONUnMarshal.
Note: The use of TDictionary for this task is not required. I just cant come with something better.
For a simple case like yours, I tend to use a custom method to represent my object in JSON. But, if you want to create reverter and converter, you should read this article:
http://www.danieleteti.it/?p=146
Another option is TSuperObject which has the ability to marshal to/from JSON using RTTI:
type
TData = record
str: string;
int: Integer;
bool: Boolean;
flt: Double;
end;
var
ctx: TSuperRttiContext;
data: TData;
obj: ISuperObject;
begin
ctx := TSuperRttiContext.Create;
try
data := ctx.AsType<TData>(SO('{str: "foo", int: 123, bool: true, flt: 1.23}'));
obj := ctx.AsJson<TData>(data);
finally
ctx.Free;
end;
end;
Related
We are using the Spring4D nullable types (which are records, not objects) in some of our business objects that need to be parsed to JSON.
When the nullable type field has no value, there are 2 options that would be okay in our case:
The field is not present in the JSON
The field is present in the JSON with value null
I am trying to make this work by using a TJSONInterceptor subclass.
For example for the TNullableInteger:
I want to create an interceptor that will be used when the field has a NullableIntegerAttribute (derived from JsonReflectAttribute), in which case my TNullableIntegerInterceptor will be used.
The problem is that I don't quite know which convertertype and revertertype to use in this case because the nullable types are record types and not object types.
Does anyone have any experience with parsing record types in Delphi?
Or are there other ways to achieve this?
Any guidance would be much appreciated.
UPDATE
In the meantime I have managed to make interceptors for most of the TNullable types that we use.
Example for TNullableInteger:
{ JsonNullableIntegerAttribute }
constructor JsonNullableIntegerAttribute.Create;
begin
inherited Create(ctObject, rtString, TNullableIntegerInterceptor);
end;
{ TNullableIntegerInterceptor }
constructor TNullableIntegerInterceptor.Create;
begin
inherited;
ConverterType := ctObject;
ReverterType := rtString;
end;
function TNullableIntegerInterceptor.ObjectConverter(Data: TObject; Field: string): TObject;
var
ctx: TRTTIContext;
Int: TNullableInteger;
begin
Int := ctx.GetType(Data.ClassType).GetField(Field).GetValue(Data).AsType<TNullableInteger>;
If Int.HasValue
then begin
StringProxy.Value := IntToStr(Int.GetValueOrDefault);
Result := StringProxy;
end
else Result := nil;
end;
procedure TNullableIntegerInterceptor.StringReverter(Data: TObject; Field, Arg: string);
var
ctx: TRTTIContext;
Value: TValue;
Int: Integer;
begin
If TryStrToInt(Arg, Int)
then begin
TValue.Make<TNullableInteger>(Int, Value);
ctx.GetType(Data.ClassType).GetField(Field).SetValue(Data, Value);
end;
end;
The only type that is still a problem is TNullableBoolean.
For the unmarshalling the procedure TJONUnMarshal.PopulateFields checks if there is a reverter available but then, depending on the revertertype, it only handles TJSONArray, TJSONObject, TJSONString and TJSONNull. Boolean values in JSON are of type TJSONBool and are not handled. Instead I get the error 'JSON object expected for field fieldname in JSON true'
Also, for the serialization, I don't know how I can achieve that my JSON looks like
"FieldName": true
instead of
"FieldName": "true"
Passing a boolean as a string in JSON is not allowed by our backend software. So for now I am using a rather stupid workaround in my ObjectConverter method by returning "BOOLEAN_TRUE_IDENTIFIER" or "BOOLEAN_FALSE_INDENTIFIER" which I replace by true or false in the resulting JSON just before sending it to our backend.
If anyone would have a solution for this, it would make me very happy :-)
In the meantime we will try to avoid using NullableBoolean variables.
type
tbet = record
fteam1: string;
fteam2: string;
fskor: string
end;
type
tdeltio = class
private
fusername: string;
fpassword: string;
fbet: array of tbet;
fprice: currency;
end;
deltio := Tdeltio.Create;
deltio.fusername := 'Vanias';
deltio.fpassword := '12345';
deltio.fprice := '70';
SetLength(deltio.fbet, 1);
deltio.fbet[0].fteam1 := 'Team1';
deltio.fbet[0].fteam2 := 'Team2';
deltio.fbet[0].fskor := '1-1';
var json := Tjson.ObjectToJsonString(deltio);
json result is like that:
{"username":"Vanias","password":"12345","bet":[["Team1","Team2","1-1"]],"price":70}
My problem is I expected something like this instead:
{"username":"Vanias","password":"12345","bet":[{"Team1":"Team1","Team2":"Team2","skor":"1-1"}],"price":70}
Why does the record type not have the property names? Ι know I can use a class for the tbet type, but I prefer a record type.
TJson is hard-coded to marshal a record type as a JSON array, not as a JSON object. This is by design, and you cannot change this behavior.
So, either use a class type instead, or else don't use ObjectToJsonString() at all. There are plenty of alternative approaches available to get the output you want, such as by using TJSONObject and TJSONArray directly, or by using a 3rd party JSON library that supports record types as JSON objects.
I have in Delphi XE4, TSuperObject:
TAspTransactionBasicData = Class(TObject)
Currency : Byte;
Amount : Currency;
constructor Create(aCurrency: Byte; aAmount: Currency);
end;
TStartWorkflowWithBasicData = Class(TObject)
AdditionalData : TAspTransactionBasicData; // here it is as an Object
TypeOfWorkflow : Byte;
constructor Create(aAdditionalData: TAspTransactionBasicData; aTypeOfWorkflow: Byte);
function toJSon(TObject:TStartWorkflowWithBasicData):String;
end;
and the ToJSon function puts the object into JSON:
function TStartWorkflowWithBasicData.toJSon(TObject:TStartWorkflowWithBasicData):String;
Var
JSon: ISuperObject;
RttiCont: TSuperRttiContext;
begin
Result := '';
RttiCont := TSuperRttiContext.Create;
JSon := RttiCont.AsJson<TStartWorkflowWithBasicData>(TObject); // Insert the object into JSON
Result := JSon.AsJSon(False);
RttiCont.Free;
end;
and I would also need the exact opposite, a function fromJSon, which will read the JSON (where the object was inserted) back into the object, but I'm groping...
Can you please advise me?
I can't help you with SuperObject, but kbmMW contains a very complete XML, JSON, YAML, BSON, MessagePack + CSV and in next release also TXT (fixed format) serializer/deserializer and object marshaller/unmarshaller.
It will easily convert both ways, and even between formats.
kbmMW Community Edition contains all (except the new TXT format), and is free, even for commercial use (limited by license).
https://components4developers.blog/2019/03/11/rest-easy-with-kbmmw-24-xml_json_yaml_to_object_conversion/
I've noticed that TJSONObject.AddPair functions results Self instead of the newly created object:
For example, in System.JSON unit I see the following code:
function TJSONObject.AddPair(const Str: string; const Val: string): TJSONObject;
begin
if (not Str.IsEmpty) and (not Val.IsEmpty) then
AddPair(TJSONPair.Create(Str, Val));
Result := Self;
end;
I was expecting something like that:
function TJSONObject.AddPair(const Str: string; const Val: string): TJSONObject;
begin
if (not Str.IsEmpty) and (not Val.IsEmpty) then
Result := AddPair(TJSONPair.Create(Str, Val));
else
Result := nil;
end;
I find this very unusual, is it a Delphi XE7 bug or is there any technical/practical reason why they did that?
Returning Self is common coding pattern called fluent interface.
It allows you to continue with calls to the same object, creating chain of methods without the need to reference object variable for every call. That makes code more readable, on the other hand it is harder to debug.
var
Obj: TJSONObject;
begin
Obj := TJSONObject.Create
.AddPair('first', 'abc')
.AddPair('second', '123')
.AddPair('third', 'aaa');
...
end;
would be equivalent of
var
Obj: TJSONObject;
begin
Obj := TJSONObject.Create;
Obj.AddPair('first', 'abc');
Obj.AddPair('second', '123');
Obj.AddPair('third', 'aaa');
...
end;
And the generated JSON object will look like:
{
"first": "abc",
"second": "123",
"third": "aaa"
}
That kind of coding style is more prevalent in languages with automatic memory management, because you don't need to introduce intermediate variables.
For instance, if you need JSON string you would use following construct:
var
s: string;
begin
s := TJSONObject.Create
.AddPair('first', 'abc')
.AddPair('second', '123')
.AddPair('third', 'aaa')
.Format(2);
...
end;
The problem with the above code in Delphi is that it creates memory leak as you don't have ability to release intermediate object. Because of that, it is more common to use fluent interface pattern in combination with reference counted classes where automatic memory management will handle releasing of any intermediary object instances.
There is an inconsistency of how SuperObject and TJson.ObjectToJsonObject represent certain parts of a class (i.e. Record fields). Let's have the following code snippet:
Uses rest.json, superobject;
type
TSimplePersonRec = record
FirstName: string;
LastName: string;
Age: byte;
end;
TSimplePerson = class
protected
FPersonRec: TSimplePersonRec;
public
property personRecord: TSimplePersonRec read FPersonRec write FPersonRec;
end;
// ...
{ Public declarations }
function toJson_SO(simplePerson: TSimplePerson): string;
function toJson_Delphi(simplePerson: TSimplePerson): string;
// ...
function TForm1.toJson_Delphi(simplePerson: TSimplePerson): string;
begin
result := tjson.Format(TJson.ObjectToJsonObject(simplePerson));
end;
function TForm1.toJson_SO(simplePerson: TSimplePerson): string;
var
so: ISuperObject;
ctx: TSuperRttiContext;
begin
ctx := TSuperRttiContext.Create;
try
so := ctx.AsJson<TSimplePerson>( simplePerson );
finally
ctx.Free;
end;
result := so.AsJSon(true, true);
end;
// ...
procedure TForm1.Button3Click(Sender: TObject);
var
spr: TSimplePersonRec;
sp: TSimplePerson;
begin
spr.FirstName := 'John';
spr.LastName := 'Doe';
spr.Age := 39;
sp := TSimplePerson.Create;
sp.personRecord := spr;
memo1.Lines.Add(#13'--- SuperObject ---'#13);
memo1.Lines.Add(toJson_SO(sp));
memo1.Lines.Add(#13'--- Delphi ---'#13);
memo1.Lines.Add(toJson_Delphi(sp));
end;
The OUTPUT is:
--- SuperObject ---
{
"FPersonRec": {
"LastName": "Doe",
"Age": 39,
"FirstName": "John"
}
}
--- Delphi ---
{
"personRec":
[
"John",
"Doe",
39
]
}
What's the reason for Delphi to represent records as JSON array? Is there a public standard or suggestion leading to this?
Note:
For me it's more natural to represent records with {key: value} notation instead of array. Not knowing the key name to witch the value belongs may have strange results during deserialization. For Example during deserialization I could pass a new class with the same layout, containing a record with different memory layout. In this case the values will be randomly assigned or an AV could occur?
UPDATE:
I'm using Delphi XE7.
Also I found this of json.org:
JSON is built on two structures:
A collection of name/value pairs. In various languages, this is realized as an object, record, struct, dictionary, hash table, keyed
list, or associative array.
An ordered list of values. In most
languages, this is realized as an array, vector, list, or sequence.
So probably the question is more about is this a bug in TJson unit?
The Delphi output is legal JSON. Internally, REST.TJson is hard-coded to marshal a record as a JSON array. That is simply how it is implemented, it is by design, not a bug. It is just another way to represent data. SuperObject chooses to be more explicit. That is also OK. Two different implementations, two different representations. The JSON spec allows for both.