Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Mailing lists / lazarus / 2018-07

Convert record to JSON?

22 messages · 5 people · –

Reply by email Subscribe to this list Your subscription

Read-only archive. Use j and k to move between messages.

Bo Berglund

I have an application that manages the configuration of an embedded device, which communicates over TCP/IP. Currently I have implmented a protocol with a command identifier for each data item and the way to send the data is by sending a packet with the command ID plus the data to the open socket.

This is less than optimal but was done when the config item number was low, but it seems to increase over time so I want to find a better way.

I have just looked into fpJSON a bit (read the wiki but no coding yet).

I have this question:
The config data are stored in a record and consists of a mix of data types like strings and numbers as well as booleans. Is there a simple way to encode the record as a JSON message to be sent to the device with all of the information in one packet? If so how would one go about coding that transformation?

If possible I would like it to be transparent such that if the record is extended later it would automatically include the new members?

Bo Berglund Developer in Sweden

Reply by email

AlexeyT
to Bo Berglund
The config data are stored in a record and consists of a mix of data types like strings and numbers as well as booleans. Is there a simple way to encode the record as a JSON message

Yes, it is. You can write to json values by path: /dir1/dir2/dir3/key,

so for your record, make string paths for your values: /myrec/keybool ;
/myrec/keyint ; ...
Regards, Alexey

Reply by email

Bo Berglund
to AlexeyT
AlexeyT wrote 10 lines
> The config data are stored in a record and consists of a mix of data > types like strings and numbers as well as booleans. > Is there a simple way to encode the record as a JSON message Yes, it is. You can write to json values by path: /dir1/dir2/dir3/key, so for your record, make string paths for your values: /myrec/keybool ; /myrec/keyint ; ...

I am not sure I understand your suggestion...

I was thinking something like this (based on an example in the wiki (http://wiki.freepascal.org/Streaming_JSON):

type TMyRecord = record
    checksum: word;
    ssid: AnsiString;
    passwd: AnsiString;
    macaddr: AnsiString;
    addr: TIpAddress;
    baud: integer;
    tcpport: word;
    mode: byte;
    channel: byte;
    hidden: byte;  // hidden (1) or visible (0)
    fixedaddress: byte; //Station mode fixed addr instead of DHCP
    numsensors: byte; //Number of active DHT sensors (0..3)
    dhtinterval: word;  // Interval between DHT sensor readings (min)
    host: AnsiString;
    reserved: array[0..18] of byte;
  end;
function RecToJSON(RC: TMyrecord): string;
var
  JS: TJSONStreamer;
begin
  JS := TJSONStreamer.Create(NIL);
  try
    JS.Options := JS.Options + [jsoTStringsAsArray];
    Result := JS.RecordToJSONString(RC);  //Seems not to exist...
  finally
    JS.Free;
  end;
end;

So I have two issues here:
1) It seems like there is no RecordToJSONString method only ObjectToJSONString.

2) Do I have to convert my record to an object just to be able to use JSON with it?

I want to avoid having to maintain a function where every member of the record will be individually handled by name, then I don't gain anything by going for JSON regarding future additions. I want the output of the RecToJSON function to contain the field names and current values of the record without ever changing this function in the future when new fields are added....

Is this at all possible?

Bo Berglund Developer in Sweden

Reply by email

Michael Van Canneyt
to Bo Berglund
Bo Berglund wrote 66 lines
On Fri, 20 Jul 2018 20:09:25 +0300, AlexeyT via Lazarus wrote: > >> The config data are stored in a record and consists of a mix of data >> types like strings and numbers as well as booleans. >> Is there a simple way to encode the record as a JSON message > > Yes, it is. You can write to json values by path: /dir1/dir2/dir3/key, > > so for your record, make string paths for your values: /myrec/keybool ; > /myrec/keyint ; ... > I am not sure I understand your suggestion... I was thinking something like this (based on an example in the wiki (http://wiki.freepascal.org/Streaming_JSON): type TMyRecord = record checksum: word; ssid: AnsiString; passwd: AnsiString; macaddr: AnsiString; addr: TIpAddress; baud: integer; tcpport: word; mode: byte; channel: byte; hidden: byte; // hidden (1) or visible (0) fixedaddress: byte; //Station mode fixed addr instead of DHCP numsensors: byte; //Number of active DHT sensors (0..3) dhtinterval: word; // Interval between DHT sensor readings (min) host: AnsiString; reserved: array[0..18] of byte; end; function RecToJSON(RC: TMyrecord): string; var JS: TJSONStreamer; begin JS := TJSONStreamer.Create(NIL); try JS.Options := JS.Options + [jsoTStringsAsArray]; Result := JS.RecordToJSONString(RC); //Seems not to exist... finally JS.Free; end; end; So I have two issues here: 1) It seems like there is no RecordToJSONString method only ObjectToJSONString. 2) Do I have to convert my record to an object just to be able to use JSON with it? I want to avoid having to maintain a function where every member of the record will be individually handled by name, then I don't gain anything by going for JSON regarding future additions. I want the output of the RecToJSON function to contain the field names and current values of the record without ever changing this function in the future when new fields are added.... Is this at all possible?

For classes it is possible, but not for records (yet). See the jsonrtti unit.

Michael.

Reply by email

AlexeyT
to Bo Berglund: On Fri, 20 Jul 2018 20:09:25 +0300, AlexeyT via Lazarus I am not sure I understand your…
Bo Berglund wrote
So I have two issues here: 1) It seems like there is no RecordToJSONString method only ObjectToJSONString.

No function; I was suggesting to make it (func) to your record type (each rec type has its new function). So record type will convert to

{

  "myrec": {

     "int_var": 10,

    "bool_var": False,

    "str_var": "ddd"

   }

}

Regards, Alexey

Reply by email

Bo Berglund
to Bo Berglund: On Fri, 20 Jul 2018 20:09:25 +0300, AlexeyT via Lazarus I am not sure I understand your…

On Fri, 20 Jul 2018 19:33:19 +0200, Bo Berglund via Lazarus wrote:

I changed my code now so my recrd is instead an object:

type TMyRecord = Class(TObject)
    checksum: word;
    ssid: AnsiString;
    passwd: AnsiString;
    macaddr: AnsiString;
    addr: TIpAddress;
    baud: integer;
    tcpport: word;
    mode: byte;
    channel: byte;
    hidden: byte;  // hidden (1) or visible (0)
    fixedaddress: byte; //Station mode fixed addr instead of DHCP
    numsensors: byte; //Number of active DHT sensors (0..3)
    dhtinterval: word;  // Interval between DHT sensor readings (min)
    host: AnsiString;
  end;

I also changed my function to create the JSON string:

function RecToJSON(RC: TMyrecord): string;
var
  JS: TJSONStreamer;
begin
  JS := TJSONStreamer.Create(NIL);
  try
    JS.Options := JS.Options + [jsoTStringsAsArray];
    Result := JS.ObjectToJSONString(RC);
  finally
    JS.Free;
  end;
end;

When I run this code in my existing application I can step into the line

   Result := JS.ObjectToJSONString(RC);

and hover the mouse over RC and it displays the correct values for all fields of the object.

But when the line executes the result is a string with only this empty content:

{}

Why is this?
What have I done wrong?

Bo Berglund Developer in Sweden

Reply by email

Michael Van Canneyt
to Bo Berglund
Bo Berglund wrote 50 lines
On Fri, 20 Jul 2018 19:33:19 +0200, Bo Berglund via Lazarus wrote: I changed my code now so my recrd is instead an object: type TMyRecord = Class(TObject) checksum: word; ssid: AnsiString; passwd: AnsiString; macaddr: AnsiString; addr: TIpAddress; baud: integer; tcpport: word; mode: byte; channel: byte; hidden: byte; // hidden (1) or visible (0) fixedaddress: byte; //Station mode fixed addr instead of DHCP numsensors: byte; //Number of active DHT sensors (0..3) dhtinterval: word; // Interval between DHT sensor readings (min) host: AnsiString; end; I also changed my function to create the JSON string: function RecToJSON(RC: TMyrecord): string; var JS: TJSONStreamer; begin JS := TJSONStreamer.Create(NIL); try JS.Options := JS.Options + [jsoTStringsAsArray]; Result := JS.ObjectToJSONString(RC); finally JS.Free; end; end; When I run this code in my existing application I can step into the line Result := JS.ObjectToJSONString(RC); and hover the mouse over RC and it displays the correct values for all fields of the object. But when the line executes the result is a string with only this empty content: {} Why is this? What have I done wrong?

Because RTTI is made only for published properties.

So, you must make it published properties:

Type

{ needed if you want to descend from TObject.
You can also descend from TPersistent. }

{$M+}
   TMyRecord = Class(TObject)
   Published
     Property checksum: word Read FChecksum Write FCheckSum;
     // etc.
   end;

Michael.

Reply by email

Bo Berglund
to Michael Van Canneyt
Michael Van Canneyt wrote 18 lines
> What have I done wrong? Because RTTI is made only for published properties. So, you must make it published properties: Type { needed if you want to descend from TObject. You can also descend from TPersistent. } {$M+} TMyRecord = Class(TObject) Published Property checksum: word Read FChecksum Write FCheckSum; // etc. end;

I changed my definition as above:

  {$M+}
  TEspConfiguration = Class(TObject)  //object
  published
    checksum: word;
    ssid: AnsiString;
    passwd: AnsiString;
    macaddr: AnsiString;
    addr: TIpAddress;
    baud: integer;
    ....
  end;

but then when I compile I get this error (14 times):

wificommhandler.pas(81,5) Error: Symbol cannot be published, can be
only a class

Do I need to declare the fields as private first so they can later be declared published?

Or are these fields (simple variables and ansistrings) incompatible with the fpc JSON handling? (Must be a class...)

Maybe it is simpler after all to code this without using fpcjson. The JSON structure is not that complicated to handle, especially for writing....

Bo Berglund Developer in Sweden

Reply by email

Sven Barth
to Bo Berglund
Bo Berglund wrote 49 lines
On Sat, 21 Jul 2018 09:29:29 +0200 (CEST), Michael Van Canneyt via Lazarus wrote: >> What have I done wrong? > Because RTTI is made only for published properties. > > So, you must make it published properties: > > Type > > { needed if you want to descend from TObject. > You can also descend from TPersistent. } > > {$M+} > TMyRecord = Class(TObject) > Published > Property checksum: word Read FChecksum Write FCheckSum; > // etc. > end; > I changed my definition as above: {$M+} TEspConfiguration = Class(TObject) //object published checksum: word; ssid: AnsiString; passwd: AnsiString; macaddr: AnsiString; addr: TIpAddress; baud: integer; .... end; but then when I compile I get this error (14 times): wificommhandler.pas(81,5) Error: Symbol cannot be published, can be only a class Do I need to declare the fields as private first so they can later be declared published? Or are these fields (simple variables and ansistrings) incompatible with the fpc JSON handling? (Must be a class...) Maybe it is simpler after all to code this without using fpcjson. The JSON structure is not that complicated to handle, especially for writing....

Look at the code that Michael wrote. He used properties on purpose, because only *properties* can be published while not being a class type. Also even if you don't use ObjectToJSONString you can still use fpJSON as that deals with the whole writing/reading part. You only need to serialize/deserialize your record manually.

That said please find attached a unit that serializes (nearly) any record to JSON and reads it back again. It might be buggy, but a quick test with your example record worked. It's compatible with both 3.0.4 as well as 3.1.1. Variant records work correctly as well as thankfully managed types can't be used with them thus simply writing/reading the memory multiple times works, at least as long as no one modified the streamed data. :P

As it's two class helpers you can use them like this (Note: for the test I implemented an "=" operator overload for your record):

=== code begin ===

uses
   fpjson, fpjson.helper, fpjsonrtti;
var
   streamer: TJSONStreamer;
   destreamer: TJSONDeStreamer;
   s: TJSONStringType;
   t1, t2: TMyRecord;
begin
   // init t1
   // ...
   streamer := TJSONStreamer.Create(Nil);
   try
     s := streamer.RecordToJSONString(@t1, TypeInfo(t1));
     Writeln('Serialized record:');
     Writeln(s);
   finally
     streamer.Free;
   end;
   Writeln;
   destreamer := TJSONDeStreamer.Create(Nil);
   try
     destreamer.JSONToRecord(s, @t2, TypeInfo(t2));
     Writeln('Equal deserialized records: ', BoolToStr(t1 = t2, True));
   finally
     destreamer.Free;
   end;
end.

=== code end ===

The output then looks like this:

=== output begin ===

Serialized record:
{ "Field1" : 17185, "Field2" : "Blubb", "Field3" : "Bla", "Field4" : "54:43:67:94:30:59", "Field5" : { "Field1" : 127, "Field2" : 0, "Field3" : 0, "Field4" : 1, "Field5" : 16777343 }, "Field6" : 115200, "Field7" : 80, "Field8" : 66, "Field9" : 2, "Field10" : 1, "Field11" : 3, "Field12" : 28, "Field13" : 21044, "Field14" : "World", "Field15" : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }

Equal deserialized records: True

=== output end ===

Note: As the RTTI for records (currently) does not contain the field names index values are used.

Generic methods in trunk can even improve this a bit more (at least once I've fixed the bug I've found there):

=== code begin ===

s := streamer.RecordToJSONString(@t1, TypeInfo(t1));
// becomes
s := streamer.specialize RecordToJSONString<TMyRecord>(t1);
destreamer.JSONToRecord(s, @t2, TypeInfo(t2));
// becomes
destreamer.specialize JSONToRecord<TMyRecord>(s, t2);

=== code end ===

@Michael: maybe we can integrate this in fpjsonrtti directly?

Regards,
Sven
-------------- next part --------------
{
JSON RTTI (de)streamer for records
Copyright (c) 2018 by Sven Barth

This file is licensed under the LGPL with static linking exception as used by Free Pascal; see COPYING.FPC of a Free Pascal distribution for details about the copyright.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

 **********************************************************************}
unit fpjson.helper;
{$mode objfpc}{$H+}
interface

{$ifndef ver3_0}
{ there still seems to be a bug in trunk regarding overloads with non generic methods, so disable them for now }
{.$define enable_gen_funcs}
{$endif}

uses
  fpjson, fpjsonrtti, typinfo;
type
  TJSONStreamerHelper = class helper for TJSONStreamer
  private
    function StreamArray(aPointer: Pointer; aTypeInfo: PTypeInfo): TJSONArray;
    function StreamField(aPointer: Pointer; aTypeInfo: PTypeInfo; const aName: String): TJSONData;
  public
    function RecordToJSON(aRecord: Pointer; aTypeInfo: PTypeInfo): TJSONObject;
{$ifdef enable_gen_funcs}
    generic function RecordToJSON<T>(constref aRecord: T): TJSONObject; inline;
{$endif}
    function RecordToJSONString(aRecord: Pointer; aTypeInfo: PTypeInfo): TJSONStringType;
{$ifdef enable_gen_funcs}
    generic function RecordToJSONString<T>(constref aRecord: T): TJSONStringType; inline;
{$endif}
  end;
  TJSONDeStreamerHelper = class helper for TJSONDeStreamer
  private
    procedure RestoreArray(aPointer: Pointer; aTypeInfo: PTypeInfo; aArrayData: TJSONArray);
    procedure DoRestoreField(aPointer: Pointer; aTypeInfo: PTypeInfo; aFieldData: TJSONData; const aName: String);
    procedure RestoreField(aPointer: Pointer; aTypeInfo: PTypeInfo; aFieldData: TJSONData; const aName: String);
  public
    procedure JSONToRecord(const aJSON: TJSONStringType; aRecord: Pointer; aTypeInfo: PTypeInfo);
    procedure JSONToRecord(const aJSON: TJSONObject; aRecord: Pointer; aTypeInfo: PTypeInfo);
{$ifdef enable_gen_funcs}
    generic procedure JSONToRecord<T>(const aJSON: TJSONStringType; out aRecord: T);
    generic procedure JSONToRecord<T>(const aJSON: TJSONObject; out aRecord: T);
{$endif}
  end;
implementation
uses
  SysUtils;
resourcestring
  SErrUnknownFieldKind        = 'Unknown field kind for field: %s';
  SErrUnsupportedFieldKind    = 'Unsupported field kind for field: %s';
  SErrArrayCountMismatch      = 'Array count does not match; expected count of %d, but got %d';
  SErrUnSupportedEnumDataType = 'Unsupported JSON type for enumerated property "%s" : "%s"';
  SErrUnsupportedJSONType     = 'Cannot destream object from JSON data of type "%s"';
type
  TSet = set of 0..31;

{ TJSONDeStreamerHelper }

procedure TJSONDeStreamerHelper.RestoreArray(aPointer: Pointer;
  aTypeInfo: PTypeInfo; aArrayData: TJSONArray);
var
  td: PTypeData;
  elsize: LongInt;
  i: LongInt;
begin
  { for now we stream the array as a flat array even if it's a multi dim one }
  td := GetTypeData(aTypeInfo);
  elsize := td^.ArrayData.Size div td^.ArrayData.ElCount;
  if td^.ArrayData.ElCount  aArrayData.Count then
    Error(sErrArrayCountMismatch, [td^.ArrayData.ElCount, aArrayData.Count]);
  for i := 0 to td^.ArrayData.ElCount - 1 do begin
    DoRestoreField(PByte(aPointer) + elsize * i, td^.ArrayData.ElType, aArrayData.Items[i], 'Array ' + IntToStr(i));
  end;
end;
procedure TJSONDeStreamerHelper.DoRestoreField(aPointer: Pointer;
  aTypeInfo: PTypeInfo; aFieldData: TJSONData; const aName: String);
var
  td: PTypeData;
  procedure SetIntegerField;
  begin
    case td^.OrdType of
      otSByte: PInt8(aPointer)^ := aFieldData.AsInteger;
      otUByte: PUInt8(aPointer)^ := aFieldData.AsInteger;
      otSWord: PInt16(aPointer)^ := aFieldData.AsInteger;
      otUWord: PUInt16(aPointer)^ := aFieldData.AsInteger;
      otSLong: PInt32(aPointer)^ := aFieldData.AsInteger;
      otULong: PUInt32(aPointer)^ := aFieldData.AsInteger;
{$ifndef ver3_0}
      otSQWord: PInt64(aPointer)^ := aFieldData.AsInt64;
      otUQWord: PUInt64(aPointer)^ := aFieldData.AsQWord;
{$endif}
    end;
  end;
  procedure SetOrdValue(aValue: LongInt);
  begin
    case td^.OrdType of
      otSByte: PInt8(aPointer)^ := aValue;
      otUByte: PUInt8(aPointer)^ := aValue;
      otSWord: PInt16(aPointer)^ := aValue;
      otUWord: PUInt16(aPointer)^ := aValue;
      otSLong: PInt32(aPointer)^ := aValue;
      otULong: PUInt32(aPointer)^ := aValue;
{$ifndef ver3_0}
      otSQWord: PInt64(aPointer)^ := aValue;
      otUQWord: PUInt64(aPointer)^ := aValue;
{$endif}
    end;
  end;
  procedure SetFloatField(aValue: Extended);
  begin
    case td^.FloatType of
      ftSingle: PSingle(aPointer)^ := aValue;
      ftDouble: PDouble(aPointer)^ := aValue;
      ftExtended: PExtended(aPointer)^ := aValue;
      ftComp: PComp(aPointer)^ := Comp(aValue);
      ftCurr: PCurrency(aPointer)^ := Currency(aValue);
    end;
  end;
var
  js: TJSONStringType;
  a: TJSONArray;
  s, i, j: LongInt;
begin
  td := GetTypeData(aTypeInfo);
  case aTypeInfo^.Kind of
    tkUnknown :
      Error(SErrUnknownFieldKind, [aName]);
    tkInteger :
      SetIntegerField;
    tkInt64 :
      PInt64(aPointer)^ := aFieldData.AsInt64;
    tkEnumeration :
      begin
        if aFieldData.JSONType = jtNumber then
          i := aFieldData.AsInteger
        else if aFieldData.JSONType = jtString then
          i := GetEnumValue(aTypeInfo, aFieldData.AsString)
        else
          Error(SErrUnSupportedEnumDataType,[aName, JSONTypeName(aFieldData.JSONType)]);
        SetOrdValue(i);
      end;
    tkFloat :
      begin
        if (aTypeInfo = TypeInfo(TDateTime)) and (aFieldData.JSONType = jtString) then
          SetFloatField(ExtractDateTime(aFieldData.AsString))
        else
          SetFloatField(aFieldData.AsFloat)
      end;
    tkSet :
      if aFieldData.JSONType = jtString then
        SetOrdValue(StringToSet(aTypeInfo, aFieldData.AsString))
      else if aFieldData.JSONType = jtArray then begin
        a := aFieldData as TJSONArray;
        s := 0;
        for i := 0 to A.Count-1 do begin
          if a.Types[i] = jtNumber then
            j := a.Integers[i]
          else
            j := GetEnumValue(td^.CompType, a.Strings[i]);
          TSet(S) := TSet(S) + [j];
        end;
        SetOrdValue(s);
      end;
    tkChar:
      begin
        js := aFieldData.AsString;
        if js  '' then
          SetOrdValue(Ord(js[1]));
      end;
    tkSString:
      PShortString(aPointer)^ := aFieldData.AsString;
    tkLString,
    tkAString:
      PAnsiString(aPointer)^ := aFieldData.AsString;
    tkWString :
      PWideString(aPointer)^ := aFieldData.AsUnicodeString;
    tkVariant:
      PVariant(aPointer)^ := JSONToVariant(aFieldData);
    tkWChar :
      begin
        js := aFieldData.AsString;
        if js  '' then
          SetOrdValue(Ord(js[1]));
      end;
    tkBool :
      SetOrdValue(Ord(aFieldData.AsBoolean));
    tkQWord :
      PQWord(aPointer)^ := aFieldData.AsQWord;
    tkArray:
      RestoreArray(aPointer, aTypeInfo, aFieldData as TJSONArray);
    tkRecord:
      JSONToRecord(aFieldData as TJSONObject, aPointer, aTypeInfo);
    tkClass,
    tkObject,
    tkInterface,
    tkDynArray,
    tkInterfaceRaw,
    tkProcVar,
    tkMethod :
      Error(SErrUnsupportedFieldKind, [aName]);
    tkUString :
      PUnicodeString(aPointer)^ := aFieldData.AsUnicodeString;
    tkUChar:
      begin
        js := aFieldData.AsString;
        if js  '' then
          SetOrdValue(Ord(js[1]));
      end;
  end;
end;
procedure TJSONDeStreamerHelper.RestoreField(aPointer: Pointer;
  aTypeInfo: PTypeInfo; aFieldData: TJSONData; const aName: String);
begin
  try
    DoRestoreField(aPointer, aTypeInfo, aFieldData, aName);
  except
    if not (jdoIgnorePropertyErrors in Options) then
      raise;
  end;
end;
procedure TJSONDeStreamerHelper.JSONToRecord(const aJSON: TJSONStringType;
  aRecord: Pointer; aTypeInfo: PTypeInfo);
var
  d: TJSONData;
begin
  d := ObjectFromString(aJSON);
  try
    If d.JSONType = jtObject then
      JSONToRecord(d as TJSONObject, aRecord, aTypeInfo)
    else
      Error(SErrUnsupportedJSONType, [JSONTypeName(d.JSONType)]);
  finally
    d.Free;
  end;
end;
procedure TJSONDeStreamerHelper.JSONToRecord(const aJSON: TJSONObject;
  aRecord: Pointer; aTypeInfo: PTypeInfo);
var
  td: PTypeData;
  mf: PManagedField;
  i, j: LongInt;
  name: TJSONStringType;
begin
  if not Assigned(aJSON) or not Assigned(aRecord) or not Assigned(aTypeInfo) then
    Exit;
  if aTypeInfo^.Kind  tkRecord then
    Exit;
  td := GetTypeData(aTypeInfo);
{$ifdef ver3_0}
  { ToDo: check for targets with FPC_REQUIRES_PROPER_ALIGNMENT set }
  mf := PManagedField(PByte(@td^.ManagedFldCount) + SizeOf(td^.ManagedFldCount));
{$else}
  mf := PManagedField(AlignTypeData(PByte(@td^.TotalFieldCount) + SizeOf(td^.TotalFieldCount)));
{$endif}
  for i := 0 to {$ifdef ver3_0}td^.ManagedFldCount{$else}td^.TotalFieldCount{$endif} - 1 do begin
    name := 'Field' + IntToStr(i + 1);
    j := aJSON.IndexOfName(name, jdoCaseInsensitive in Options);
    if j >= 0 then
      RestoreField(PByte(aRecord) + mf^.FldOffset, mf^.TypeRef, aJSON.Items[j], name);
    Inc(mf);
  end;
end;
{$ifdef enable_gen_funcs}
generic procedure TJSONDeStreamerHelper.JSONToRecord<T>(const aJSON: TJSONStringType; out aRecord: T);
begin
  JSONToRecord(aJSON, @aRecord, TypeInfo(aRecord));
end;
generic procedure TJSONDeStreamerHelper.JSONToRecord<T>(const aJSON: TJSONObject; out aRecord: T);
begin
  JSONToRecord(aJSON, @aRecord, TypeInfo(aRecord));
end;
{$endif}

{ TJSONStreamerHelper }

function TJSONStreamerHelper.StreamArray(aPointer: Pointer; aTypeInfo: PTypeInfo
  ): TJSONArray;
var
  td: PTypeData;
  elsize: LongInt;
  ad: TJSONData;
  i: LongInt;
begin
  Result := TJSONArray.Create;
  { for now we stream the array as a flat array even if it's a multi dim one }
  td := GetTypeData(aTypeInfo);
  elsize := td^.ArrayData.Size div td^.ArrayData.ElCount;
  for i := 0 to td^.ArrayData.ElCount - 1 do begin
    try
      ad := StreamField(PByte(aPointer) + elsize * i, td^.ArrayData.ElType, 'Array ' + IntToStr(i));
      Result.Add(ad);
    except
      FreeAndNil(Result);
      raise;
    end;
  end;
end;
function TJSONStreamerHelper.StreamField(aPointer: Pointer;
  aTypeInfo: PTypeInfo; const aName: String): TJSONData;
var
  td: PTypeData;
  function GetIntegerField: TJSONData;
  begin
    case td^.OrdType of
      otSByte:
        Result := TJSONIntegerNumber.Create(PInt8(aPointer)^);
      otUByte:
        Result := TJSONIntegerNumber.Create(PUInt8(aPointer)^);
      otSWord:
        Result := TJSONIntegerNumber.Create(PInt16(aPointer)^);
      otUWord:
        Result := TJSONIntegerNumber.Create(PUInt16(aPointer)^);
      otSLong:
        Result := TJSONIntegerNumber.Create(PInt32(aPointer)^);
      otULong:
        Result := TJSONIntegerNumber.Create(PUInt32(aPointer)^);
{$ifndef ver3_0}
      otSQWord:
        Result := TJSONInt64Number.Create(PInt64(aPointer)^);
      otUQWord:
        Result := TJSONQWordNumber.Create(PUInt64(aPointer)^);
{$endif}
    end;
  end;
  function GetOrdValue: LongInt;
  begin
    case td^.OrdType of
      otSByte:
        Result := PInt8(aPointer)^;
      otUByte:
        Result := PUInt8(aPointer)^;
      otSWord:
        Result := PInt16(aPointer)^;
      otUWord:
        Result := PUInt16(aPointer)^;
      otSLong:
        Result := PInt32(aPointer)^;
      otULong:
        Result := LongInt(PUInt32(aPointer)^);
{$ifndef ver3_0}
      otSQWord:
        Result := LongInt(PInt64(aPointer)^);
      otUQWord:
        Result := LongInt(PUInt64(aPointer)^);
{$endif}
    end;
  end;
  function GetFloatValue: Extended;
  begin
    case td^.FloatType of
      ftSingle:
        Result := PSingle(aPointer)^;
      ftDouble:
        Result := PDouble(aPointer)^;
      ftExtended:
        Result := PExtended(aPointer)^;
      ftComp:
        Result := PComp(aPointer)^;
      ftCurr:
        Result := PCurrency(aPointer)^;
    end;
  end;
var
  s, i: LongInt;
  f: Extended;
begin
  Result := Nil;
  td := GetTypeData(aTypeInfo);
  Case aTypeInfo^.Kind of
    tkUnknown :
      Error(SErrUnknownFieldKind, [aName]);
    tkInteger :
      Result := GetIntegerField;
    tkEnumeration :
      begin
        i := GetOrdValue;
        if jsoEnumeratedAsInteger in Options then
          Result := TJSONIntegerNumber.Create(i)
        else
          Result := TJSONString.Create(GetEnumName(aTypeInfo,i));
      end;
    tkFloat :
      begin
        f := GetFloatValue;
        if (aTypeInfo=TypeInfo(TDateTime)) and (jsoDateTimeAsString in Options) then
          Result := FormatDateProp(f)
        else
          Result := TJSONFloatNumber.Create(f);
      end;
    tkSet :
      if jsoSetAsString in Options then
        Result := TJSONString.Create(SetToString(aTypeInfo, GetOrdValue, jsoSetBrackets in Options))
      else begin
        s := GetOrdValue;
        Result := TJSONArray.Create;
        try
          for i:=0 to 31 do
            if i in TSet(s) then
              if jsoSetEnumeratedAsInteger in Options then
                TJSONArray(Result).Add(i)
              else
                TJSONArray(Result).Add(GetEnumName(td^.CompType, i));
        except
          FreeAndNil(Result);
          raise;
        end;
      end;
    tkChar:
      Result := TJSONString.Create(Char(GetOrdValue));
    tkSString :
      Result := TJSONString.Create(PShortString(aPointer)^);
    tkLString,
    tkAString :
      Result := TJSONString.Create(PAnsiString(aPointer)^);
    tkWString :
      Result := TJSONString.Create(PWideString(aPointer)^);
    tkVariant:
      Result := StreamVariant(PVariant(aPointer)^);
    tkClass:
      Result := StreamClassProperty(TObject(aPointer));
    tkWChar :
      Result := TJSONString.Create(WideChar(GetOrdValue));
    tkBool :
      Result := TJSONBoolean.Create(GetOrdValue0);
    tkInt64 :
      Result := TJSONInt64Number.Create(PInt64(aPointer)^);
    tkQWord :
      Result := TJSONFloatNumber.Create(PQWord(aPointer)^);
    tkRecord :
      Result := RecordToJSON(aPointer, aTypeInfo);
    tkArray :
      Result := StreamArray(aPointer, aTypeInfo);
    tkObject,
    tkInterface,
    tkDynArray,
    tkInterfaceRaw,
    tkProcVar,
    tkMethod :
      Error(SErrUnsupportedFieldKind, [aName]);
    tkUString :
      Result := TJSONString.Create(PUnicodeString(aPointer)^);
    tkUChar:
      Result := TJSONString.Create(UnicodeChar(GetOrdValue));
  end;
end;
function TJSONStreamerHelper.RecordToJSON(aRecord: Pointer; aTypeInfo: PTypeInfo
  ): TJSONObject;
var
  td: PTypeData;
  mf: PManagedField;
  i: LongInt;
  name: TJSONStringType;
  fd: TJSONData;
begin
  if not Assigned(aRecord) or not Assigned(aTypeInfo) then
    Exit(Nil);
  if aTypeInfo^.Kind  tkRecord then
    Exit(Nil);
  Result := TJSONObject.Create;
  try
    td := GetTypeData(aTypeInfo);
{$ifdef ver3_0}
    { ToDo: check for targets with FPC_REQUIRES_PROPER_ALIGNMENT set }
    mf := PManagedField(PByte(@td^.ManagedFldCount) + SizeOf(td^.ManagedFldCount));
{$else}
    mf := PManagedField(AlignTypeData(PByte(@td^.TotalFieldCount) + SizeOf(td^.TotalFieldCount)));
{$endif}
    for i := 0 to {$ifdef ver3_0}td^.ManagedFldCount{$else}td^.TotalFieldCount{$endif} - 1 do begin
      name := 'Field' + IntToStr(i + 1);
      fd := StreamField(PByte(aRecord) + mf^.FldOffset, mf^.TypeRef, name);
      if Assigned(fd) then begin
        if jsoLowerPropertyNames in Options then
          name := LowerCase(name);
        Result.Add(name, fd);
      end;
      Inc(mf);
    end;
  except
    FreeAndNil(Result);
    raise;
  end;
end;
{$ifdef enable_gen_funcs}
generic function TJSONStreamerHelper.RecordToJSON<T>(constref aRecord: T): TJSONObject;
begin
  Result := RecordToJSON(@aRecord, TypeInfo(aRecord));
end;
{$endif}
function TJSONStreamerHelper.RecordToJSONString(aRecord: Pointer;
  aTypeInfo: PTypeInfo): TJSONStringType;
var
  o: TJSONData;
begin
  o := RecordToJSON(aRecord, aTypeInfo);
  try
    if jsoUseFormatString in Options then
      Result := o.FormatJSON
    else
      Result := o.AsJSON;
  finally
    o.Free;
  end;
end;
{$ifdef enable_gen_funcs}
generic function TJSONStreamerHelper.RecordToJSONString<T>(constref aRecord: T): TJSONStringType;
begin
  Result := RecordToJSONString(@aRecord, TypeInfo(aRecord));
end;
{$endif}
end.

Reply by email

Michael Van Canneyt
to Sven Barth
Sven Barth wrote 13 lines
=== code begin === s := streamer.RecordToJSONString(@t1, TypeInfo(t1)); // becomes s := streamer.specialize RecordToJSONString<TMyRecord>(t1); destreamer.JSONToRecord(s, @t2, TypeInfo(t2)); // becomes destreamer.specialize JSONToRecord<TMyRecord>(s, t2); === code end === @Michael: maybe we can integrate this in fpjsonrtti directly?

:-) :-) :-)

I am glad you proposed it, saves me the trouble of asking it. I was planning to extend JSONRTTI so it can handle more cases (I want to replace the rest JSON handling with it), so this can definitely be included.

I'll study your code and get back to you.
Thanks for the sample code !! :)

Michael.

Reply by email

Sven Barth
to Michael Van Canneyt
Michael Van Canneyt wrote 28 lines
On Sat, 21 Jul 2018, Sven Barth via Lazarus wrote: > === code begin === > > s := streamer.RecordToJSONString(@t1, TypeInfo(t1)); > // becomes > s := streamer.specialize RecordToJSONString<TMyRecord>(t1); > > destreamer.JSONToRecord(s, @t2, TypeInfo(t2)); > // becomes > destreamer.specialize JSONToRecord<TMyRecord>(s, t2); > > === code end === > > @Michael: maybe we can integrate this in fpjsonrtti directly? :-) :-) :-) I am glad you proposed it, saves me the trouble of asking it. I was planning to extend JSONRTTI so it can handle more cases (I want to replace the rest JSON handling with it), so this can definitely be included. I'll study your code and get back to you. Thanks for the sample code !! :)

There are definitely still some problems with it, e.g. a field of type TObject (or any descendant) currently can't be deserialized as I didn't want to rewrite DeStreamClassProperty. Then there are the sets which for non-published properties can be greater than 32-bit (up to 256-bit currently). Support for dynamic arrays could be added as well. Also I didn't check whether all types serialize/deserialize correctly, e.g. especially the special floating point types Comp and Currency. You'd need to check what Get-/SetFloatProp are doing there.

Also I noticed a problem that's also in the TObject based streaming code: WideChar and UnicodeChar are deserialized incorrectly as TJSONStringType is of type UTF8String and thus js[1] might be an invalid character if it's Codepoint happens to be > $7f.

Regards,
Sven

Reply by email

Bo Berglund
to Sven Barth: Look at the code that Michael wrote. He used properties on purpose, because only…
Sven Barth wrote
Look at the code that Michael wrote. He used properties on purpose, because only *properties* can be published while not being a class type.

Sorry, did not fully note the property keyword!
Not doing that made it easier to support both object and record through conditional defines without changing the body of the code...

But it now means I have to use a new structure like this:

TIpAddress = array[0..3] of byte;
TEspConfiguration = Class(TObject)
private
  Fchecksum: word;
  Fssid: AnsiString;
  Fpasswd: AnsiString;
  Fmacaddr: AnsiString;
  Fhost: AnsiString;
  Faddr: TIpAddress;
  Fbaud: integer;
  Ftcpport: word;
  Fmode: byte;
  Fchannel: byte;
  Fhidden: byte;
  Ffixedaddress: byte;
  Fnumsensors: byte;
  Fdhtinterval: word;
published
  property checksum: word read Fchecksum write Fchecksum;
  property ssid: AnsiString read Fssid write Fssid ;
  property passwd: AnsiString read Fpasswd write Fpasswd;
  property macaddr: AnsiString read Fmacaddr write Fmacaddr;
  property host: AnsiString read Fhost write Fhost;
  property addr: TIpAddress read Faddr write Faddr;
  property baud: integer read Fbaud write Fbaud;
  property tcpport: word read Ftcpport write Ftcpport;
  property mode: byte read Fmode write Fmode;
  property channel: byte read Fchannel write Fchannel;
  property hidden: byte read Fhidden write Fhidden;
  property fixedaddress: byte read Ffixedaddress write Ffixedaddress;
  property numsensors: byte read Fnumsensors write Fnumsensors;
  property dhtinterval: word read Fdhtinterval write Fdhtinterval;
end;

Can the JSON handlers deal with the array type TIpAddress?

And how do I "plug" the fpjson.helper.pp code into my project?

Bo Berglund Developer in Sweden

Reply by email

Bo Berglund
to Bo Berglund
Bo Berglund wrote
Can the JSON handlers deal with the array type TIpAddress?

Well, it trurns out that this fails earlier on when compiling... So I have:

type
  TIpAddress = array[0..3] of byte;
  {$M+}
  TEspConfiguration = Class(TObject)
  private
    Fchecksum: word;
    Fssid: AnsiString;
    ...
    Faddr: TIpAddress;  //<==
    ...
  published
    property checksum: word read Fchecksum write Fchecksum;
    property ssid: AnsiString read Fssid write Fssid ;
    ...
    property addr: TIpAddress read Faddr write Faddr; // <==
    ...
  end

When compiling I get the following error on the "property addr" line:

wificommhandler.pas(97,33) Error: This kind of property cannot be
published

If I replace the type declaration TIpAddress with array[0..3] of byte it just adds another error message...

Can published properties only be simple variable types not including arrays?
Seems odd since an AnsiString, which is accepted, is just an array of AnsiChar.

Bo Berglund Developer in Sweden

Reply by email

Michael Van Canneyt
to Bo Berglund
Bo Berglund wrote 31 lines
On Sun, 22 Jul 2018 00:22:58 +0200, Bo Berglund via Lazarus wrote: > Can the JSON handlers deal with the array type TIpAddress? Well, it trurns out that this fails earlier on when compiling... So I have: type TIpAddress = array[0..3] of byte; {$M+} TEspConfiguration = Class(TObject) private Fchecksum: word; Fssid: AnsiString; ... Faddr: TIpAddress; //<== ... published property checksum: word read Fchecksum write Fchecksum; property ssid: AnsiString read Fssid write Fssid ; ... property addr: TIpAddress read Faddr write Faddr; // <== ... end When compiling I get the following error on the "property addr" line: wificommhandler.pas(97,33) Error: This kind of property cannot be published

Only dynamic arrays can be published.

Show quoted text 8 lines
If I replace the type declaration TIpAddress with array[0..3] of byte it just adds another error message... Can published properties only be simple variable types not including arrays? Seems odd since an AnsiString, which is accepted, is just an array of AnsiChar.

Yes, but it is a dynamic array.

Michael.

Reply by email

Sven Barth
to Bo Berglund: On Sat, 21 Jul 2018 17:47:54 +0200, Sven Barth via Lazarus Sorry, did not fully note the…

Bo Berglund via Lazarus schrieb am So., 22.
Juli 2018, 00:23:

And how do I "plug" the fpjson.helper.pp code into my project?

Simply add the unit to your project and use it where you use fpjsonrtti (in addition to that unit!). The methods are then part of TJSONStreamer and TJSONDeStreamer.

The TIpAddress should work with that code as well.

Regards,
Sven

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.lazarus-ide.org/pipermail/lazarus/attachments/20180722/d10e5505/attachment.html>

Reply by email

Bo Berglund
to Michael Van Canneyt: Only dynamic arrays can be published. Yes, but it is a dynamic array. Michael.
Michael Van Canneyt wrote
Only dynamic arrays can be published.

So I changed the declaration:

TIpAddress = array of byte;

Then added a constructor to the class where I set the length to 4:

constructor TEspConfiguration.Create;
begin
  SetLength(Faddr,4);
end;

This removed the errors I had before but popped up another one in my main code:

  String2IP(FConfRec.addr, edWiFiAddress.Text);  //<== Error here
formmainconfig.pas(240,26) Error: Can't take the address of constant
expressions

This function converts an IP address as string into the byte array. The data comes from an editbox where the user enters the IP address required:

function TfrmMainConfig.String2IP(var IP: TIpAddress; Addr: string):
boolean;
var
  Lst: TStringList;
  i: integer;
  t: integer;
begin
  Result := false;
  Lst := TStringList.Create;
  try
    Lst.StrictDelimiter := true;
    Lst.Delimiter := '.';
    Lst.DelimitedText := Addr;
    if Lst.Count  4 then exit; //First requirement, 4 positions
    for i := 0 to 3 do
    begin
      t := StrToIntDef(Lst[i],-1);
      if (t<0) and (t>255) then exit; //Second, must be byte sized
      IP[i] := t and $FF;
    end;
    Result := true;
  finally
    Lst.Free;
  end;
end;

So now it looks like I am stuck between two exclusive requirements...

Bo Berglund Developer in Sweden

Reply by email

Michael Van Canneyt
to Bo Berglund
Bo Berglund wrote 23 lines
On Sun, 22 Jul 2018 07:52:40 +0200 (CEST), Michael Van Canneyt via Lazarus wrote: > Only dynamic arrays can be published. > So I changed the declaration: TIpAddress = array of byte; Then added a constructor to the class where I set the length to 4: constructor TEspConfiguration.Create; begin SetLength(Faddr,4); end; This removed the errors I had before but popped up another one in my main code: String2IP(FConfRec.addr, edWiFiAddress.Text); //<== Error here formmainconfig.pas(240,26) Error: Can't take the address of constant expressions

That is because you cannot pass the array as a var argument. Just remove the var, for a dynamic array it is not required anyway.

Michael.

Reply by email

Bo Berglund
to Michael Van Canneyt
Michael Van Canneyt wrote
That is because you cannot pass the array as a var argument. Just remove the var, for a dynamic array it is not required anyway.

I did so and the compile error disappeared.
However, now that there are no compile time errors instead I get a runtime exception inside the call to ObjectToJSONString here:

function TConfigCommHandler.ConfigAsJSON(RC: TEspConfiguration):
string;
{Create a JSON verson of the configuration record}
var
  JS: TJSONStreamer;
begin
  JS := TJSONStreamer.Create(NIL);
  try
    JS.Options := JS.Options + [jsoTStringsAsArray];
    Result := JS.ObjectToJSONString(RC);
  finally
    JS.Free;
  end;
end;

The Exception message is:
' : Unsupported property kind for property: "addr"'

So it is still not supported....

Bo Berglund Developer in Sweden

Reply by email

Luca Olivetti
to Sven Barth: There are definitely still some problems with it, e.g. a field of type TObject (or any…

El 21/7/18 a les 18:26, Sven Barth via Lazarus ha escrit:

Show quoted text 9 lines
> !! :) There are definitely still some problems with it, e.g. a field of type TObject (or any descendant) currently can't be deserialized as I didn't want to rewrite DeStreamClassProperty. Then there are the sets which for non-published properties can be greater than 32-bit (up to 256-bit currently). Support for dynamic arrays could be added as well. Also I didn't check whether all types serialize/deserialize correctly, e.g. especially the special floating point types Comp and Currency. You'd need to check what Get-/SetFloatProp are doing there.

I tested this unit and I see that it doesn't manage bitpacked records inside the record, and the field names are "Field1", "Field2", etc. (I wanted to test what happens if I add fields to a record then try to deserialize into it a previously serialized version, but without real field names I suppose that wouldn't work properly).

Bye

Luca Olivetti Wetron Automation Technology http://www.wetron.es/ Tel. +34 93 5883004 (Ext.3010) Fax +34 93 5883007

Reply by email

Sven Barth
to Luca Olivetti

Luca Olivetti via lazarus schrieb am Di.,
26. Feb. 2019, 09:34:

Show quoted text 18 lines
El 21/7/18 a les 18:26, Sven Barth via Lazarus ha escrit: >> !! :) > There are definitely still some problems with it, e.g. a field of type > TObject (or any descendant) currently can't be deserialized as I didn't > want to rewrite DeStreamClassProperty. Then there are the sets which for > non-published properties can be greater than 32-bit (up to 256-bit > currently). Support for dynamic arrays could be added as well. > Also I didn't check whether all types serialize/deserialize correctly, > e.g. especially the special floating point types Comp and Currency. > You'd need to check what Get-/SetFloatProp are doing there. I tested this unit and I see that it doesn't manage bitpacked records inside the record, and the field names are "Field1", "Field2", etc. (I wanted to test what happens if I add fields to a record then try to deserialize into it a previously serialized version, but without real field names I suppose that wouldn't work properly).

I didn't test bitbacked records so those might indeed fail. And field names are currently not available. This would require the extended RTTI which is still work-in-progress.

Regards,
Sven

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.lazarus-ide.org/pipermail/lazarus/attachments/20190226/2d4b6c8f/attachment.html>

Reply by email

Luca Olivetti
to Sven Barth

El 26/2/19 a les 11:11, Sven Barth via lazarus ha escrit:

I didn't test bitbacked records so those might indeed fail.

The RTTI information gives TkUnknown for a bitpacked record.

And field names are currently not available. This would require the extended RTTI which is still work-in-progress.

No big issue, I will serialize those records manually. Being lazy I would have avoided it ;-)

Bye

Luca Olivetti Wetron Automation Technology http://www.wetron.es/ Tel. +34 93 5883004 (Ext.3010) Fax +34 93 5883007

Reply by email

Sven Barth
to Luca Olivetti

Luca Olivetti via lazarus schrieb am Di.,
26. Feb. 2019, 12:05:

Show quoted text 7 lines
El 26/2/19 a les 11:11, Sven Barth via lazarus ha escrit: > > I didn't test bitbacked records so those might indeed fail. The RTTI information gives TkUnknown for a bitpacked record.

Ah, right, they don't get RTTI at all... 😅

Regards,
Sven

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.lazarus-ide.org/pipermail/lazarus/attachments/20190226/fae33db1/attachment.html>

Reply by email