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

6
  • 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. Commented Jan 30, 2023 at 9: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) Commented Jan 30, 2023 at 10:02
  • 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. Commented Jan 30, 2023 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. Commented Jan 30, 2023 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. Commented Jan 31, 2023 at 8:00

3 Answers 3

3

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.

Sign up to request clarification or add additional context in comments.

Comments

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.

5 Comments

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.
4. I think TAPIPerson.AfterConstruction is not a proper place to initialize instance fields.
@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.
@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).
Delphi JSON library serializes instance fileds, not properties. See Delphi Rest.JSON JsonToObject only works with f variables.
-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)

3 Comments

Thats not correct, you say about Null-Terminated Strings, but not all strings in delphi is null terminated. docwiki.embarcadero.com/RADStudio/Alexandria/en/…
@OleksandrMorozevych yes ALL string in Delphi are null terminated. docwiki.embarcadero.com/RADStudio/Alexandria/en/… : 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.
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;

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.