0

I saw in this question: Empty string becomes null when passed from Delphi to C# as a function argument that Delphi's empty string value in reality is just a null-pointer - which I understand the reasoning behind.

I do have an issue though as I am developing a Web API in Delphi and I am having trouble implementing a PATCH endpoint and I wondered if anyone has had the same issue as me.

If i have a simple resource Person which looks like this.

{
  "firstName": "John",
  "lastName": "Doe",
  "age": 44
}

and simply want to change his lastName property using a PATCH document - I would sent a request that looks like this:

{
  "lastName": "Smith"
}

Now - in my api, using Delphis System.JSON library I would just check if the request has the firstName and age properties before setting them in the request handler which sets the properties in an intermediate object PersonDTO, but later I have to map these values to the actual Person instance - and here comes my issue:

When mapping between multiple objects I cannot tell if a string is empty because it was never set (and should be treated as null) or was explicitly set to '' to remove a property from my resource - How do I circumvent this?

 if personDTO.FirstName <> '' then
   personObject.FirstName := personDTO.FirstName;

Edit: I have considered setting the strings to #0 in the DTO's constructor to distinguish between null and '' but this is a large (1M line) code base, so I would prefer to find a robust generic way of handling these scenarios

Matt Baech
  • 404
  • 11
  • 23
  • I believe that you should not use an intermediate object and instead update the `Person` object directly - and only those properties which have been provided in the JSON. Pascal strings are actually pointers to a data structure which holds another pointer to the actual text and also the length of the string. So you may try to distinguish between a String which is NIL and a String which points to "" - but I don't think this is a robust way. – IVO GELOV Jan 30 '23 at 09:09
  • @IVOGELOV Unfortunately this is not an option for me as I oversimplified the situation a bit in my example. We have a layer of abstraction in our public api that separates two entity types which we then later merge into one. (Person + Employment = employee) – Matt Baech Jan 30 '23 at 10:02
  • I understand what you are saying - but I think I could've framed my question better. I am fully aware of how strings work in Delphi. I am also convinced that I can't be the first person to have this issue - I am simply looking for inspiration to solve the issue. Maybe I should phrase it like so: If a property is optional in the API definition, how would I be able to tell if the property was not passed and is `''` by default (meaning do not change this property)- or that the property `''` WAS passed and the user wishes to set it to an empty string – Matt Baech Jan 30 '23 at 12:32
  • 1
    @IVOGELOV "*Pascal strings are actually pointers to a data structure **which holds another pointer** to the actual text*" - that is incorrect. The text is part of the data structure itself. The structure is allocated large enough to hold the full text at its end. There is no second pointer. – Remy Lebeau Jan 30 '23 at 15:33
  • 1
    @fpiette "*when using a string where a PChar is required. If the string is empty, the PChar will be the null pointer*" - you mean null character, not null pointer. Casting a `string` directly to a `PChar` will not produce a `nil` pointer, it will produce a pointer to a null `#0` character. To get a `nil` pointer, you have to cast the `string` to a raw `Pointer` first, then cast that to `PChar`. – Remy Lebeau Jan 30 '23 at 15:36
  • 3
    Delphi strings aren't nullable. You can't use a Delphi string and hope to have distinct null and empty string values. You need to use a different type in Delphi. – David Heffernan Jan 31 '23 at 08:00

3 Answers3

2

Delphi does not differentiate between an empty string and an unassigned string. They are implemented the exact same way - as a nil pointer. So, you will have to use a different type that does differentiate, such as a Variant. Otherwise, you will have to carry a separate boolean/enum flag alongside the string to indicate its intended state. Or, wrap the string value inside of a record/class type that you can set a pointer at when assigned and leave nil when unassigned.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
0

The answer is in your question itself. You need to know what has been supplied. This means that you either need to use what was actually provided to the API rather than serialising into an object (which has to include all the members of the object), or you need to serialise into an object whose members will support you knowing whether they have been set or not.

If you are serialising into an intermediate object for the API then when you come to update your actual application object you can use an assign method that only sets the members of the application object that were set in the API. Implementing these checks in the intermediate object for your API means that you won't have to change any code in the main application.

Code that suggests how you might do this:

unit Unit1;

interface

uses  Classes;

  type
  TAPIIVariableStates = (APIVarSet, APIVarIsNull);
  TAPIVariableState = Set of TAPIIVariableStates;
  TAPIString =class(TObject)
  protected
    _szString:          String;
    _MemberState:       TAPIVariableState;

    function  _GetHasBeenSet(): Boolean; virtual;
    function  _GetIsNull(): Boolean; virtual;
    function  _GetString(): String; virtual;
    procedure _SetString(szNewValue: String); virtual;

  public
    procedure  AfterConstruction(); override;

    procedure  Clear(); virtual;
    procedure  SetToNull(); virtual;

    property  Value: String read _GetString write _SetString;
    property  HasBeenSet: Boolean read _GetHasBeenSet;
    property  IsNull: Boolean read _GetIsNull;
  end;

  TAPIPerson = class(TPersistent)
  protected
    FFirstName:         TAPIString;
    FLastName:          TAPIString;
    FComments:          TAPIString;

    procedure AssignTo(Target: TPersistent); override;

    function  _GetComments(): String; virtual;
    function  _GetFirstName(): String; virtual;
    function  _GetLastName(): String; virtual;
    procedure _SetComments(szNewValue: String); virtual;
    procedure _SetFirstName(szNewValue: String); virtual;
    procedure _SetLastName(szNewValue: String); virtual;

  public
    destructor Destroy; override;
    procedure AfterConstruction(); override;

    property  FirstName: String read _GetFirstName write _SetFirstName;
    property  LastName: String read _GetLastName write _SetLastName;
    property  Comments: String read _GetComments write _SetComments;

  end;

  TApplicationPerson = class(TPersistent)
  protected
    FFirstName:         String;
    FLastName:          String;
    FComments:          String;
  public
    property  FirstName: String read FFirstName write FFirstName;
    property  LastName: String read FLastName write FLastName;
    property  Comments: String read FComments write FComments;
  end;

implementation

uses  SysUtils;

  destructor TAPIPerson.Destroy();
  begin
    FreeAndNil(Self.FFirstName);
    FreeAndNil(Self.FLastName);
    FreeAndNil(Self.FComments);
    inherited;
  end;

  procedure TAPIPerson.AfterConstruction();
  begin
    inherited;
    Self.FFirstName:=TAPIString.Create();
    Self.FLastName:=TAPIString.Create();
    Self.FComments:=TAPIString.Create();
  end;

  procedure TAPIPerson.AssignTo(Target: TPersistent);
  begin
    if(Target is TApplicationPerson) then
    begin
      if(Self.FFirstName.HasBeenSet) then
        TApplicationPerson(Target).FirstName:=Self.FirstName;
      if(Self.FLastName.HasBeenSet) then
        TApplicationPerson(Target).LastName:=Self.LastName;
      if(Self.FComments.HasBeenSet) then
        TApplicationPerson(Target).Comments:=Self.Comments;
    end
    else
      inherited;
  end;

  function TAPIPerson._GetComments(): String;
  begin
    Result:=Self.FComments.Value;
  end;

  function TAPIPerson._GetFirstName(): String;
  begin
    Result:=Self.FFirstName.Value;
  end;

  function TAPIPerson._GetLastName(): String;
  begin
    Result:=Self.FLastName.Value;
  end;

  procedure TAPIPerson._SetComments(szNewValue: String);
  begin
    Self.FComments.Value:=szNewValue;
  end;

  procedure TAPIPerson._SetFirstName(szNewValue: String);
  begin
    Self.FFirstName.Value:=szNewValue;
  end;

  procedure TAPIPerson._SetLastName(szNewValue: String);
  begin
    Self.FLastName.Value:=szNewValue;
  end;

  procedure TAPIString.AfterConstruction();
  begin
    inherited;
    Self._MemberState:=[APIVarIsNull];
  end;

  procedure TAPIString.Clear();
  begin
    Self._szString:='';
    Self._MemberState:=[APIVarIsNull];
  end;

  function TAPIString._GetHasBeenSet(): Boolean;
  begin
    Result:=(APIVarSet in Self._MemberState);
  end;

  function TAPIString._GetIsNull(): Boolean;
  begin
    Result:=(APIVarIsNull in Self._MemberState);
  end;

  function TAPIString._GetString(): String;
  begin
    Result:=Self._szString;
  end;

  procedure TAPIString._SetString(szNewValue: String);
  begin
    Self._szString:=szNewValue;
    Include(Self._MemberState, APIVarSet);
    (* optionally treat an emoty strung and null as the same thing
    if(Length(Self._szString)=0) then
      Include(Self._MemberState, APIVarIsNull)
    else
      Exclude(Self._MemberState, APIVarIsNull); *)
  end;

  procedure TAPIString.SetToNull();
  begin
    Self._szString:='';
    Self._MemberState:=[APIVarSet, APIVarIsNull];
  end;

end.

Using AssignTo in the TAPIPerson means that if your TApplicationPerson object derives from TPersistent (and has a properly implemented Assign method) then you can just use <ApplicationPersonObject>.Assign(<APIPersonObject>) to update just those fields which have changed. Otherwise you need a public method in the TAPIPerson that will update the TApplicationPerson appropriately.

Rob Lambden
  • 2,175
  • 6
  • 15
  • **1.** Using class type for `TAPIString` seems overkill to me. I think that `record` would fit better, because it doesn't require explicit finalization in `TAPIPerson.Destroy`. **2.** I would change member `APIVarIsNull` of `TAPIIVariableStates` to `APIVarIsNotNull` or similar so it wouldn't require explicit initialization in `TAPIString.AfterConstruction`. **3.** Have you tried serializing an instance of `TAPIPerson` to and from JSON? In order to properly serialize `TAPIString` as JSON string value you need to register custom converter. – Peter Wolf Jan 31 '23 at 13:00
  • **4.** I think `TAPIPerson.AfterConstruction` is not a proper place to initialize instance fields. – Peter Wolf Jan 31 '23 at 13:00
  • @PeterWolf thanks for your comments. As `TAPIPerson` is an object with public properties which are strings with read and write accessors creating it from a JSON string is the same process as any other object with public properties which are strings. As the OP is already doing this it does not need to be part of my answer. I have only shown `TAPIString` but the OP would likely also need a `TAPIInteger` (and so on) having a class hierarchy seems a more elegant way to address this, but of course other opinions are available. – Rob Lambden Jan 31 '23 at 13:38
  • @PeterWolf - I read somewhere a recommendation of initialising object in `AfterConstruction` when I was relatively new to `Delphi` and I have got into that habit. `AfterConstruction` is going to be called anyway (if it's an object) so other than clarity of expression it doesn't seem to make much difference where it goes. I think the most important thing is to be consistent within a code base. That will be different for different people, but for me that means initialising in `AfterConstruction` (checking what other classes in the hierarchy may or may not have already set). – Rob Lambden Jan 31 '23 at 13:43
  • Delphi JSON library serializes instance fileds, not properties. See [Delphi Rest.JSON JsonToObject only works with f variables](https://stackoverflow.com/questions/31778518/delphi-rest-json-jsontoobject-only-works-with-f-variables). – Peter Wolf Jan 31 '23 at 13:47
-1

in Delphi String is an array, but it's an array a little longer than the actual count of characters. For exemple in Delphi string always end up with the #0 at high(myStr) + 1. this is need when casting the string to pchar. if in your flow you don't plan to cast the string to pchar then you can write a special char in this "invisible" characters to distinguish between null and empty (actually i never tested this solution)

zeus
  • 12,173
  • 9
  • 63
  • 184
  • 1
    Thats not correct, you say about Null-Terminated Strings, but not all strings in delphi is null terminated. https://docwiki.embarcadero.com/RADStudio/Alexandria/en/String_Types_(Delphi) – Oleksandr Morozevych Jan 31 '23 at 08:05
  • @OleksandrMorozevych yes ALL string in Delphi are null terminated. https://docwiki.embarcadero.com/RADStudio/Alexandria/en/Internal_Data_Formats_(Delphi) : The NULL character at the end of a string memory block is automatically maintained by the compiler and the built-in string handling routines. This makes it possible to typecast a string directly to a null-terminated string. – zeus Feb 01 '23 at 09:11
  • 1
    Read doc carefully. procedure TForm2.Button1Click(Sender: TObject); var ss : ShortString; begin ss := 'abcd'; // ss[0] = 4 // ss[5] = random, in my case is 'S'; end; – Oleksandr Morozevych Feb 06 '23 at 12:16