Jump to content
vfbb

iPub Refit - REST API in simple way

Recommended Posts

"but it is likely to work on several previous versions, probably XE7 or newer"

 

I don't think so as I can't find System.JSON.Serializers under Berlin but under Tokyo. (Also lacks from the lib dir)

Am I missing this from my installation or is it first appeared in the Tokyo release?

 

Is it possible to decorate the custom nullable fields that this internal serializer/reverter can translate them?

A one-time-registered type converter/reverter would be even better.

 

 

Edited by Attila Kovacs

Share this post


Link to post
21 minutes ago, Attila Kovacs said:

I don't think so as I can't find System.JSON.Serializers under Berlin but under Tokyo.

I don't have older versions installed to be able to test, or do backwards compatibility, but it looks like you're right, delphi 10.2 tokyo or newer.

 

22 minutes ago, Attila Kovacs said:

Is it possible to decorate the custom nullable fields that this internal serializer/reverter can translate them?

A one-time-registered type converter/reverter would be even better.

What exactly did you want? The json serialization of delphi is very malleable, you do not need to declare all the necessary fields and you can also use all existing attributes in System.Json.Serializers.pas.
 

29 minutes ago, Attila Kovacs said:

A one-time-registered type converter/reverter would be even better.

It's a great option, I will implement it today.

Share this post


Link to post

Either they forgot to ship System.Json.Serializers.pas for Berlin, or was not finished to the time, I've checked 2 installations and it's missing in both!

I'm still fumbling around with REST.JsonReflect.pas I'm freaking out right now, the header says "Copyright(c) 2016-2018".

Edited by Attila Kovacs

Share this post


Link to post
25 minutes ago, vfbb said:

It's a great option, I will implement it today.

This is it. You don't have to. System.JSON.Serializers has a JsonConverter() attribute for it.

You can even easily implement that only the fields which are - changed or not null or whatever - should be marshalled.

Brrrrr.

 

Looks like I was wrong on that. Looks like it's still a decorating-horror. I'll need more investigation on the code.

Edited by Attila Kovacs

Share this post


Link to post

TJsonDynamicContractResolver.SetTypeConverter

 

Okay, everything clear now.

This unit is based on the very same called in .net.

But .net has nullable types and they also have:

Quote

How to ignore a property in class if null, using json.net?

-If you create the serializer yourself there is a NullValueHandling property which you can set to ignore.

 

Well, and what do we have?

Yet Another Useless Serializer.

 

I'll stick with my modified REST.JsonReflect.pas where I can register converters and reverters on PTypeInfo base.

 

Edited by Attila Kovacs

Share this post


Link to post

@Attila Kovacs I added support to register custom json converters.

 

About the nullables, is very easy to implement the nullable and the converters. See one simple solution:

 

uses
  System.Rtti, System.TypInfo, System.JSON.Serializers, System.JSON.Readers,
  System.JSON.Writers, System.JSON.Types, iPub.Rtl.Refit;
  
type
  TNullable<T> = record
    IsNull: Boolean;
    Value: T;
  end;

  TNullableConverter<T> = class(TJsonConverter)
  public
    procedure WriteJson(const AWriter: TJsonWriter; const AValue: TValue; const ASerializer: TJsonSerializer); override;
    function ReadJson(const AReader: TJsonReader; ATypeInf: PTypeInfo; const AExistingValue: TValue;
      const ASerializer: TJsonSerializer): TValue; override;
    function CanConvert(ATypeInf: PTypeInfo): Boolean; override;
  end;
  
 { TNullableConverter<T> }

function TNullableConverter<T>.CanConvert(ATypeInf: PTypeInfo): Boolean;
begin
  Result := ATypeInf = TypeInfo(TNullable<T>);
end;

function TNullableConverter<T>.ReadJson(const AReader: TJsonReader;
  ATypeInf: PTypeInfo; const AExistingValue: TValue;
  const ASerializer: TJsonSerializer): TValue;
var
  LNullable: TNullable<T>;
begin
  if AReader.TokenType = TJsonToken.Null then
  begin
    LNullable.IsNull := True;
    LNullable.Value := Default(T);
  end
  else
  begin
    LNullable.IsNull := False;
    LNullable.Value := AReader.Value.AsType<T>;
  end;
  TValue.Make<TNullable<T>>(LNullable, Result);
end;

procedure TNullableConverter<T>.WriteJson(const AWriter: TJsonWriter;
  const AValue: TValue; const ASerializer: TJsonSerializer);
var
  LNullable: TNullable<T>;
  LValue: TValue;
begin
  LNullable := AValue.AsType<TNullable<T>>;
  if LNullable.IsNull then
    AWriter.WriteNull
  else
  begin
    TValue.Make<T>(LNullable.Value, LValue);
    AWriter.WriteValue(LValue);
  end;
end;

initialization
  GRestService.RegisterConverters([TNullableConverter<string>,
    TNullableConverter<Byte>, TNullableConverter<Word>,
    TNullableConverter<Integer>, TNullableConverter<Cardinal>,
    TNullableConverter<Single>, TNullableConverter<Double>,
    TNullableConverter<Int64>, TNullableConverter<UInt64>,
    TNullableConverter<TDateTime>, TNullableConverter<Boolean>,
    TNullableConverter<Char>]);
end.

To use just replace the simple types to the nullable types:

  TUser = record
    Name: TNullable<string>;
    Location: string;
    Id: Integer;
    Email: TNullable<string>;
  end;
  
  [BaseUrl('https://api.github.com')]
  IGithubApi = interface(IipRestApi)
    ['{4C3B546F-216D-46D9-8E7D-0009C0771064}']
    [Get('/users/{user}')]
    function GetUser(const AUser: string): TUser;
  end;
  
procedure TForm1.FormCreate(Sender: TObject);
var
  LGithubApi: IGithubApi;
  LUser: TUser;
begin
  LGithubApi := GRestService.&For<IGithubApi>;
  LUser := LGithubApi.GetUser('viniciusfbb');
end;

You can test the code above and see the user name is not null but the user email is null.

The nullable type and the nullable converter code in this example need to be implemented by the developer, I can't put it on the library because each developer has it own implementation os nullable.

  • Like 1

Share this post


Link to post

@Attila Kovacs About the version of delphi supported, according to docwiki, the units System.JSON.XXX was introduced in Delphi 10 Seattle, but again I can’t guarantee it’s compatible as it would need to be tested on these versions.

  • Like 1

Share this post


Link to post

I don't know, I don't think that it was in Seattle, as I said, it's not in Berlin.

At least I can see now, that you can pass converters. I could not figure it out before, no time to check it now either.

 

But it looks good, there are only two more things coming to my mind which is necessary to handle all kind of json rest services.

-To be able to suppress the null values from the nullables (or a shouldmarshal callback/wallpaper)

-To be able to send and receive pure arrays "[]" , some php api's are working that strange way

 

Then it will be a really really nice allrounder.

Share this post


Link to post
4 minutes ago, Attila Kovacs said:

-To be able to send and receive pure arrays "[]" , some php api's are working that strange way

Works perfectly in both cases, with the [] or with {..},{..},{..}. In both cases your function result should be a TArray<> of a record or a class.

8 minutes ago, Attila Kovacs said:

-To be able to suppress the null values from the nullables (or a shouldmarshal callback/wallpaper)

Do you mean what happens with the nullable fields that were not received? Well, all fields are set to the default value before reading the received json, so there are several possible workarounds to ensure that the field not received is considered null. In sydney just add the class operator Initialize to set IsNull true, but in older versions, you can simply change the name of IsNull to IsValid, since the boolean fields started False, then the initial value of IsValid of nullable will be False. But there are other possibilities.

Share this post


Link to post

The other way around.

Sometimes you just have to send the values you want to change and if you send a "null" for a field the servers clears it's value on the server side too.

 

See from this .net Q/A:

Quote

 

How to ignore a property in class if null, using json.net?

 -If you create the serializer yourself there is a NullValueHandling property which you can set to ignore.

 

 

 

btw. TValue.Make<T>  is 10.4+

Share this post


Link to post

@Attila Kovacs Here one example without TValue.Make<T>, and with the nullables stating with IsNull True:

uses
  System.Rtti, System.TypInfo, System.JSON.Serializers, System.JSON.Readers,
  System.JSON.Writers, System.JSON.Types, iPub.Rtl.Refit;
  
type
  TNullable<T> = record
  strict private
    FIsNotNull: Boolean;
    function GetIsNull: Boolean;
    procedure SetIsNull(AValue: Boolean);
  public
    Value: T;
    property IsNull: Boolean read GetIsNull write SetIsNull;
  end;

  TNullableConverter<T> = class(TJsonConverter)
  public
    procedure WriteJson(const AWriter: TJsonWriter; const AValue: TValue; const ASerializer: TJsonSerializer); override;
    function ReadJson(const AReader: TJsonReader; ATypeInf: PTypeInfo; const AExistingValue: TValue;
      const ASerializer: TJsonSerializer): TValue; override;
    function CanConvert(ATypeInf: PTypeInfo): Boolean; override;
  end;
  
{ TNullable<T> }

function TNullable<T>.GetIsNull: Boolean;
begin
  Result := not FIsNotNull;
end;

procedure TNullable<T>.SetIsNull(AValue: Boolean);
begin
  FIsNotNull := not AValue;
end;

{ TNullableConverter<T> }

function TNullableConverter<T>.CanConvert(ATypeInf: PTypeInfo): Boolean;
begin
  Result := ATypeInf = TypeInfo(TNullable<T>);
end;

function TNullableConverter<T>.ReadJson(const AReader: TJsonReader;
  ATypeInf: PTypeInfo; const AExistingValue: TValue;
  const ASerializer: TJsonSerializer): TValue;
var
  LNullable: TNullable<T>;
begin
  if AReader.TokenType = TJsonToken.Null then
  begin
    LNullable.IsNull := True;
    LNullable.Value := Default(T);
  end
  else
  begin
    LNullable.IsNull := False;
    LNullable.Value := AReader.Value.AsType<T>;
  end;
  TValue.Make(@LNullable, TypeInfo(TNullable<T>), Result);
end;

procedure TNullableConverter<T>.WriteJson(const AWriter: TJsonWriter;
  const AValue: TValue; const ASerializer: TJsonSerializer);
var
  LNullable: TNullable<T>;
  LValue: TValue;
begin
  LNullable := AValue.AsType<TNullable<T>>;
  if LNullable.IsNull then
    AWriter.WriteNull
  else
  begin
    TValue.Make(@LNullable.Value, TypeInfo(T), LValue);
    AWriter.WriteValue(LValue);
  end;
end;

initialization
  GRestService.RegisterConverters([TNullableConverter<string>,
    TNullableConverter<Byte>, TNullableConverter<Word>,
    TNullableConverter<Integer>, TNullableConverter<Cardinal>,
    TNullableConverter<Single>, TNullableConverter<Double>,
    TNullableConverter<Int64>, TNullableConverter<UInt64>,
    TNullableConverter<TDateTime>, TNullableConverter<Boolean>,
    TNullableConverter<Char>]);
end.

To test:

  TUser = record
    Name: TNullable<string>;
    Location: string;
    Id: Integer;
    Email: TNullable<string>;
    NonexistentField: TNullable<Integer>;
  end;
  
  [BaseUrl('https://api.github.com')]
  IGithubApi = interface(IipRestApi)
    ['{4C3B546F-216D-46D9-8E7D-0009C0771064}']
    [Get('/users/{user}')]
    function GetUser(const AUser: string): TUser;
  end;
  
procedure TForm1.FormCreate(Sender: TObject);
var
  LGithubApi: IGithubApi;
  LUser: TUser;
begin
  LGithubApi := GRestService.&For<IGithubApi>;
  LUser := LGithubApi.GetUser('viniciusfbb');
end;

 

Edited by vfbb

Share this post


Link to post
2 hours ago, vfbb said:

and with the nullables stating with IsNull True:

Sorry I don't know what you mean and how to test serialization with an example of deserialization.

There should be an option for nullables if "null" values should be serialized or not at all, but you will find this out by yourself one day if you start using

this lib with php API's.

 

Anyway, I tried to compile the lib with Berlin and there are couple of things which are only Tokyo+. (Very far from "perhaps XE7").

(Edit: even in Tokyo, System.JSON.Serializers is so buggy that it needs several workarounds)

 

But System.JSON.Serializers.pas compiles fine under Berlin, thanks for bringing this unit to my attention.

 

 

Edited by Attila Kovacs
  • Like 1

Share this post


Link to post

Ah ok, its for creating an API consumer, 

(my first thought was something for declaring API's,, e.g. like in radserver (which we are using right now)).

 

Edited by mvanrijnen
  • Like 1

Share this post


Link to post

@Attila Kovacs You are right about compatibility, thanks for checking this out. I will put this observation. In the future I can do some backwards compatibility, but not now.

 

About the null values serialization, this library is just to consumes one rest api, then the serialization can occur just when you call a Post or Put and have the parameter ABody that is a record or a class or an array. In this specific case, the library will serialize the type as json, and in this case, the values that is null will be written to the json body without skip. The current implementation of System.JSON.Serializers don’t permit to ignore the fields with null values.

 

1 hour ago, mvanrijnen said:

Ah ok, its for creating an API consumer, 

(my first thought was something for declaring API's,, e.g. like in radserver (which we are using right now)).

 

Sorry i'll make it clearer

  • Thanks 1

Share this post


Link to post
1 hour ago, vfbb said:

The current implementation of System.JSON.Serializers don’t permit to ignore the fields with null values

Wow, that was an unexpected twist. I hope it's already on the wish list or a bug report was filed as unsatisfactory mimicking of .net.

 

 

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×