Archive

Archive for December 13, 2016

JSON persistence, when a classtype is only known at runtime

December 13, 2016 Leave a comment

One of the cool things about Delphi is that there are usually many different ways of solving a problem.

When I write code I try to make it as simple as I can. There was a time where I would jump in and try to speed things up with assembler, use exit in for/next loops to avoid setting the result more than absolutely necessary and try to squeeze every last drop of performance out of my PC.

But what about those cases where you don’t know, where you have a large inheritance chain involving many classes – all of which can be serialized and have parent/child relationships impossible to predict?

But as you grow a little bit older and perhaps a tiny bit more mature (ahem, at my rate I’ll hit puberty sometime next year) you start to value different things. Maintainability, safety and readability suddenly become an active part of your vocabulary. So you start looking at what’s already in Delphi rather than coding a faster, cooler version of the wheel.

That JSON thing

I must have seen about 100 different serialization schemes for Delphi. Some RTTI walkers, some binary only streaming, and some even hand-write every property, which to me utterly defeats the purpose of RTTI. I mean, what is the point of RTTI if you cannot automate how classes are saved? And perhaps more importantly: how classes are loaded back.

Serialization plain and simple

Serialization plain and simple

Most examples online deal with known classes. Meaning that the code expects you to hardcode the target type you want to de-serialize. Again I find this to completely defeat the purpose of both JSON, RTTI and persistence; the whole point of serialization is being able to reduce an instance to a portable, safe, readable format (string); and then re-create that instance without any loss of information from the portable format at will.

Known at runtime

Having to know exactly what class a piece of JSON text is prior to serialization is backwards, at least to my mind. There will naturally be situations where you do know exactly what class will be involved – at it may be fixed and never change (like child objects of a particular type). But what about those cases where you don’t know, where you have a large inheritance chain involving many classes – all of which can be serialized and have parent/child relationships impossible to predict?

In my case, an AST (abstract symbol tree) which is a model of a program. It’s what a parser and compiler generates before transforming it into machine-code, or in my case bytecodes and JavaScript.

As you can guess, an AST structure can contain all manner of combinations and parent/child relationships. Everything is parsed, validated and placed into the AST structure. Every parameter or return datatype is attached to its logical parent.

In short: you can forget about foresight or hardcoding anything! And if JSON is going to be of any use, it has to be able to de-serialize and recognize a class on the spot.

Autoregister

Earlier I posted an example of an attribute that autoregister a class with Delphi’s persistent framework. To explain what it means to register a class is beyond the scope of this post, but it essentially makes a class “known” to Delphi. Making Delphi able to recognize its type and re-create it by name alone. But this system is mainly used for binary storage, not JSON.

But it’s actually exactly what we need to automate JSON serialization.

Getting into it

Since I can’t really post a whole AST with nearly 100 classes here, I will just post this one class that contains the JSON storage mechanism you need. Just inherit from that class (clean it up and rename it first) and each inheritance step will actually take care of itself. It should be easy enough to extract the essence of this code.

You also want the AutoRegister Attribute I posted earlier! Also keep in mind that in this case we de-serialize to the current instance. You simply need to use Getclass() to get the classtype by name, create an instance of it and then de-serialize the JSON into a fresh new instance.

uses
  System.Rtti, System.TypInfo, System.Sysutils,
  System.Classes, System.Generics.Collections, System.Json,
  REST.Json, REST.JSon.Types, REST.JsonReflect, autoregister;

  [TQTXAutoRegister]
  TQTXASTPersistent = class(TInterfacedPersistent)
  protected
    procedure WriteObjectJSon(const Target: TJSONObject); virtual;
    procedure ReadObjectJSon(const Source: TJSONObject); virtual;
  public
    procedure Assign(Source: TPersistent); override;
    function  Stringify: string; virtual;
    procedure Parse(ObjectData: string); virtual;
    class function JSONToObject(const MetaData: string; var obj: TObject): boolean;
  end;

//##########################################################################
// TQTXASTPersistent
//##########################################################################

procedure TQTXASTPersistent.Assign(Source: TPersistent);
begin
  if assigned(source) then
  begin
    if (source is Classtype)
    or (source.InheritsFrom(ClassType)) then
    begin
      Parse( TQTXASTPersistent(Source).Stringify );
    end else
    inherited Assign(Source);
  end else
  inherited Assign(Source);
end;

function TQTXASTPersistent.Stringify: string;
var
  LObj: TJsonObject;
begin
  LObj := TJsonObject.Create;
  try
    WriteObjectJSon(LObj);
  finally
    result := TJSon.Format(LObj);
    LObj.Free;
  end;
end;

procedure TQTXASTPersistent.Parse(ObjectData: string);
var
  LSchema: TJsonObject;
  LObjData: TJsonObject;
  LEntry: TJSonObject;
  LId: string;
begin
  LId := '';

  // Parse whole schema
  LSchema := TJSonObject( TJSonObject.ParseJSONValue(ObjectData, true) );
  try
    if LSchema.Values[QualifiedClassName] <> nil then
    begin
      // Find storage entry for our class
      LEntry := TJsonObject( LSchema.GetValue(QualifiedClassName) );

      // attempt to get the identifier
      if LEntry.Values['$identifier'] <> nil then
      LId := LEntry.GetValue('$identifier').Value;

      // validate identifier
      if LId.Equals(Classname) then
      begin

        // Grab the data chunk of our entry
        LObjData := TJSonObject( LEntry.GetValue('$data') );

        // Data PTR valid?
        if LObjData <> nil then
        begin

          // Map values into our instance
          try
            ReadObjectJSon(LObjData);
          except
            on e: exception do
            raise EQTXASTJSONError.CreateFmt
            ('Serialization failed, system threw exception %s with message "%s" error',
            [e.ClassName, e.Message]);
          end;

        end else
        raise EQTXASTJSONError.CreateFmt
        ('Serialization failed, unable to find section ["%s\$data"] in JSON document error',
        [QualifiedClassName]);

      end else
      raise EQTXASTJSONError.CreateFmt
      ('Serialization failed, invalid signature, expected %s not %s error',
      [classname, LId]);

    end else
    raise EQTXASTJSONError.CreateFmt
    ('Serialization failed, unable to find section ["%s"] in JSON document error',
    [QualifiedClassName]);
  finally
    LSchema.Free;
  end;
end;

procedure TQTXASTPersistent.WriteObjectJSon(const Target: TJSONObject);
var
  LObj: TJsonObject;
begin
  LObj := TJSonObject.Create;
  LObj.AddPair('$identifier', ClassName);
  LObj.AddPair('$data', TJSon.ObjectToJsonObject(self) );
  Target.AddPair(QualifiedClassName, LObj);
  Target.AddPair('$classname$', ClassName);
end;

procedure TQTXASTPersistent.ReadObjectJSon(const Source: TJSONObject);
begin
  TJSon.JsonToObject(self, Source);
end;

// This function will create any registered class based on name.
// The class must be registered first.
class function TQTXASTPersistent.JSONToObject
      (const MetaData: string; var obj: TObject): boolean;
var
  LSchema: TJsonObject;
  LClassName: string;
  LType: TClass;
  LNameNode: TJSONValue;
  LObj: TQTXASTPersistent;
begin
  result := false;
  obj := nil;

  // Parse whole schema
  LSchema := TJSonObject( TJSonObject.ParseJSONValue(MetaData, true) );
  try
    LNameNode := LSchema.Values['$classname$'];
    if LNameNode <> nil then
    begin
      LClassName := LNameNode.Value;
      LType := GetClass(LClassName);
      if LType <> nil then
      begin
        LObj := TQTXASTPersistent( LType.Create );
        LObj.Parse(MetaData);
        obj := LObj;
        result := true;
      end;
    end;
  finally
    LSchema.Free;
  end;
end;

Autoregister attribute for Delphi

December 13, 2016 10 comments

imageWouldnt it be nice if Delphi had an attribute that would automatically register your classes? So instead of having to manually call Registerclass() you just decorate the class with an attribute and it’s automatically registered. No messing about with rouge procedure calls in the initialize section, just pretty and neat class declarations as far as the eye can see.

Well, I think that would be a good idea. And after searching for such a class without finding it (which is a bit odd since it’s such an obvious candidate for automation) – I decided to make one.

Now if you are expecting a huge unit with super-duper advanced code, I’m afraid I must disappoint you. This class is about elegance, grace and usefulness. It may be a humble class in the great scheme of things, but as is often the case in life, the simplest things can be of greatest value.

I find it very useful and I hope you find it equally helpful in your projects.

unit AutoRegister;

interface

uses
System.Rtti, System.TypInfo, System.Sysutils, System.Classes;

type

// Our fancy shmancy attribute
TAutoRegister = class(TCustomAttribute)
end;

implementation

// This procedure walks through all classtypes and isolates
// those with our TAutoRegister attribute.
// It then locates the actual classtype and registeres it
// with Delphi's persistance layer
procedure ProcessAutoRegisterAttributes;
var
  ctx : TRttiContext;
  typ : TRttiType;
  attr : TCustomAttribute;
  LRealType: TClass;
  LAccess: PTypeData;
begin
  ctx := TRttiContext.Create();
  try
    for typ in ctx.GetTypes() do
    begin
      if typ.TypeKind = tkClass then
      begin
        for attr in typ.GetAttributes() do
        begin
          if attr is TAutoRegister then
          begin
            LAccess := GetTypeData(typ.Handle);
            if LAccess <> nil then
            begin
              LRealType := LAccess^.ClassType;
              if LRealType <> nil then
              begin
                if LRealType.InheritsFrom(TPersistent)
                or LRealType.InheritsFrom(TInterfacedPersistent) then
                begin
                  RegisterClass( TPersistentClass(LRealType) );
                end;
              end;
              break;
            end;
          end;
        end;
      end;
    end;
  finally
    ctx.Free();
  end;
end;

// We want to register all the classes decorated with our little
// attribute when this unit is loaded into memory
Initialization
begin
  ProcessAutoRegisterAttributes;
end;

end.

That’s basically it. Now to use the attribute just save out the unit and include it as you would any other (just add it to the uses-clause where you need it). Then decorate the classes you want registered automatically.

Remember that only classes that inherit from TPersistent (which includes TInterfacedPersistent) can be registered.

uses AutoRegister;

type
  [TAutoRegister]
  TMyClass = class(TPeristent)
  end;

  [TAutoRegister]
  TMyCoolClass = class(TInterfacedPersistent)
  end;

And voila, no more Registerclass() calls at the bottom of your units. Just neat, clean object oriented code -and as a bonus: I find it easier to read as well.

Cheers!