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
CodePudding user response:
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.
CodePudding user response:
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)
CodePudding user response:
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.