In GraphQL all scalar types are nullable by default. In GraphQL for Delphi, when using a schema type with a nullable Int or Float field, there seems to be no built-in way to make the resolver function return null.
I modified the TSchemaFloatType.Serialize function to explicitly check for a null variant type. The updated code snippet is provided below:
function TSchemaFloatType.Serialize(const Value: TValue): TValue;
begin
if Value.IsEmpty or
((Value.Kind = tkVariant) and (Value.AsVariant = Null)) then
Exit(TValue.Empty);
if not Value.TryCast(TypeInfo(Extended), Result) then
raise EGraphQLCoercingSerialize.Create(Self.DisplayName, Value);
end;
With this change, a variant set to null in the resolver will be correctly serialized as null in the GraphQL response.
Is this functionality missing from the library, or am I misunderstanding something?
To return null values, you should just return TValue.Empty.
Can you please provide more details and exact steps to reproduce the issue you are claiming?
I modified your bookshelf demo by adding an int field to the Author schema type.
type Author {
id: ID!
name: String!
createdOn: DateTime!
someInt: Int
books: [Book!]!
}
Then I implemented that in TAuthor like so
TAuthor = class
private
FId: Integer;
FName: string;
FCreatedOn: TDateTime;
function GetBooks: TArray<TBook>;
function GetSomeInt: TValue;
public
constructor Create(AName: string);
property Books: TArray<TBook> read GetBooks;
property Id: Integer read FId;
property Name: string read FName;
property CreatedOn: TDateTime read FCreatedOn;
property SomeInt: TValue read GetSomeInt;
end;
function TAuthor.GetSomeInt: TValue;
begin
Result := TValue.Empty;
end;
This raises an exception in TSchemaIntType.Serialize because at that point the value isn't empty anymore and the function tries to cast it to an Integer. I stopped debugging at TRttiPropResolver.Resolve in GraphQL.Schema.Resolvers where the result is not resolved to empty.
To reproduce just run the application and run the query shown in the form. Also good to mention: I'm using version 1.3 of your library in Delphi 11.1. We did not get around to upgrading yet.
The type of the field is Integer, so you cannot use TValue as the value of the Delphi class property type itself.
Since Delphi has no nullable-types, you have to provide a custom field resolver for such field. The following code solves your issue:
procedure TDataModule1.GraphQLSchema1InitSchema(Sender: TObject; Schema: TSchemaDocument);
begin
Schema.Bind('Query', TQuery);
Schema.Bind('Example', TExample);
Schema.SetResolver('Example', 'someInt',
function(Args: TFieldResolverArgs): TValue
begin
Result := TValue.Empty;
end);
end;
To avoid writing separate resolvers for nullable integer and float fields, I modified the Float and Int serializers (see snippet above) to use the Variant return type, which allows for null values. Do you see any drawbacks to this approach?
You say "Since Delphi has no nullable-types...", but that is clearly not the case: Delphi supports the variant type, which is nullable. That is precisely what allows my colleague @Richard to test for null in his modified Serialize-method.
You yourself suggested using a TValue, which is also nullable(emptyable) and that would be another option.
And in fact both types work in the TSchemaIntType and TSchemFloatType-resolvers when a non null integer or float is assigned. The existing framework converts these without a problem to the correct output. The only problem is when a null or empty value is returned. I think we can agree that it would be of tremendous value if your framework would also convert nulls correctly in these kind of resolvers, since these object-type resolvers are way easier to read and manipulate than separate field-resolvers. This would greatly benefit the usability of your framework.
(Note also that for input types it is possible to use variants and TValues and when a field is missing in the input this is correctly set to null)
BTW, I traced through the code using a TValue in the resolver and, when that is set to null, when arriving in the Serialize-method, it is not longer an emtpy TValue, but actually a TValue that contains an empty TValue, which is not recognized itself as empty. So maybe code could also be added to recognize these as empty or convert them in an earlier stage to empty TValue
I was just providing a solution that could be used with the current version. But yes, in a future release with can provide a check for NULL Variant and also nested TValue.
We just have to pay attention to performance, those are very low level methods that might be called thousands of times when serializing the response, so any extra check can cause an overhead.