| Q |
A |
| Bug report? |
no |
| Feature request? |
yes |
| BC Break report? |
no |
| RFC? |
yes |
| Version/Branch |
current 1.x / master |
Summary
It would be useful to have an opt-in way to preserve the difference between:
- an input field that was not provided at all;
- an input field that was explicitly provided as
null;
- an input field that was provided with a non-null value;
when hydrating GraphQL input objects into PHP DTOs via arguments() / ArgumentsTransformer.
This is especially important for partial update / PATCH-like mutations.
Background
GraphQL input coercion already preserves this semantic difference.
According to the GraphQL specification, if a nullable input object field is not provided, no entry is added to the coerced map. If the field is explicitly provided as null, an entry is added with value null.
Reference:
https://spec.graphql.org/September2025/#sec-Input-Objects.Input-Coercion
For example:
input UpdateUserInput {
name: String
email: String
phone: String
}
Desired semantics:
updateUser(input: { name: "Alice" })
Means: update name, do not touch email or phone.
updateUser(input: { phone: null })
Means: explicitly clear phone.
Means: do not change anything.
Current behavior
When using raw args, this information is still available.
For example, with this input:
updateUser(input: {
name: "Alice"
phone: null
})
args can still represent the difference:
[
'input' => [
'name' => 'Alice',
'phone' => null,
// email is not present
],
]
So userland code can do:
array_key_exists('email', $args['input']); // false
array_key_exists('phone', $args['input']); // true
However, when using DTO hydration through arguments():
resolve: '@=mutation("update_user", arguments({"input": "UpdateUserInput"}, args))'
and a DTO like:
use Overblog\GraphQLBundle\Annotation as GQL;
#[GQL\Input]
final class UpdateUserInput
{
#[GQL\Field(type: 'String')]
public ?string $name = null;
#[GQL\Field(type: 'String')]
public ?string $email = null;
#[GQL\Field(type: 'String')]
public ?string $phone = null;
}
the resolver receives an object where both omitted fields and explicitly-null fields are represented as null:
$input->email === null;
$input->phone === null;
At this point the resolver cannot reliably distinguish:
email was omitted and should not be changed;
email was explicitly provided as null and should be cleared.
Why this matters
This makes DTO-based input objects inconvenient for partial update mutations.
For create mutations, a regular nullable DTO property works well.
For update/patch mutations, nullable DTO properties lose important GraphQL semantics. The current workaround is to avoid DTO hydration and manually inspect raw arrays with array_key_exists, but that means giving up strongly typed input DTOs in resolvers.
Another workaround is to model every nullable field as a wrapper input object in the public schema, for example:
input NullableStringInput {
value: String
}
input UpdateUserInput {
name: NullableStringInput
email: NullableStringInput
}
But this makes the public GraphQL API much less idiomatic:
updateUser(input: {
name: { value: "Alice" }
})
instead of:
updateUser(input: {
name: "Alice"
})
Proposed solution
Add an opt-in way for hydrated input DTOs to preserve omitted/null/value semantics.
One possible API could be a generic-like Omittable<T> wrapper, using PHPDoc generics for static analysis:
/**
* @var Omittable<string|null>
*/
#[GQL\Field(type: 'String')]
public Omittable $phone;
The GraphQL schema type would still be String, while the PHP DTO property would preserve whether the field was provided.
Resolver code could then look like:
public function updateUser(UpdateUserInput $input): User
{
if ($input->phone->isSet()) {
// phone was provided, either as null or as a string
$user->setPhone($input->phone->value());
}
// ...
}
Expected states:
// field was not provided
$input->phone->isSet() === false;
// field was explicitly provided as null
$input->phone->isSet() === true;
$input->phone->value() === null;
// field was provided with a value
$input->phone->isSet() === true;
$input->phone->value() === '+123';
This would be conceptually similar to gqlgen's Omittable[T]: a value plus a boolean flag indicating whether the field was provided.
Reference:
https://github.com/99designs/gqlgen/blob/master/graphql/omittable.go
Expected behavior
For this GraphQL input:
the hydrated DTO should allow resolver code to know that:
$input->phone->isSet() === false;
For this GraphQL input:
updateUser(input: { phone: null })
the hydrated DTO should allow resolver code to know that:
$input->phone->isSet() === true;
$input->phone->value() === null;
For this GraphQL input:
updateUser(input: { phone: "+123" })
the hydrated DTO should allow resolver code to know that:
$input->phone->isSet() === true;
$input->phone->value() === '+123';
Backward compatibility
This should probably be opt-in only.
Existing DTO hydration behavior should remain unchanged unless a field or input object explicitly asks for this behavior.
The opt-in mechanism could be one of:
- detecting properties typed as
Omittable;
- a field-level option;
- an input-level option;
- a custom hydrator / argument transformer extension point.
Related issue
This seems related to the broader ArgumentsTransformer discussion:
#538
That issue discusses the current limitations of ArgumentsTransformer and the need to better separate transformation and hydration. This feature request is narrower: preserving GraphQL's omitted/null/value semantics when hydrating input DTOs.
Workarounds today
The current workaround is to avoid DTO hydration for PATCH-like mutations and inspect raw arrays manually:
if (array_key_exists('phone', $args['input'])) {
$user->setPhone($args['input']['phone']);
}
This works, but it means losing typed DTOs in resolvers.
Another workaround is to expose wrapper input objects in the GraphQL schema, but that leaks a server-side implementation concern into the public API and makes the schema less idiomatic.
Summary
It would be useful to have an opt-in way to preserve the difference between:
null;when hydrating GraphQL input objects into PHP DTOs via
arguments()/ArgumentsTransformer.This is especially important for partial update / PATCH-like mutations.
Background
GraphQL input coercion already preserves this semantic difference.
According to the GraphQL specification, if a nullable input object field is not provided, no entry is added to the coerced map. If the field is explicitly provided as
null, an entry is added with valuenull.Reference:
https://spec.graphql.org/September2025/#sec-Input-Objects.Input-Coercion
For example:
Desired semantics:
Means: update
name, do not touchemailorphone.Means: explicitly clear
phone.Means: do not change anything.
Current behavior
When using raw
args, this information is still available.For example, with this input:
argscan still represent the difference:[ 'input' => [ 'name' => 'Alice', 'phone' => null, // email is not present ], ]So userland code can do:
However, when using DTO hydration through
arguments():and a DTO like:
the resolver receives an object where both omitted fields and explicitly-null fields are represented as
null:At this point the resolver cannot reliably distinguish:
emailwas omitted and should not be changed;emailwas explicitly provided asnulland should be cleared.Why this matters
This makes DTO-based input objects inconvenient for partial update mutations.
For create mutations, a regular nullable DTO property works well.
For update/patch mutations, nullable DTO properties lose important GraphQL semantics. The current workaround is to avoid DTO hydration and manually inspect raw arrays with
array_key_exists, but that means giving up strongly typed input DTOs in resolvers.Another workaround is to model every nullable field as a wrapper input object in the public schema, for example:
But this makes the public GraphQL API much less idiomatic:
instead of:
Proposed solution
Add an opt-in way for hydrated input DTOs to preserve omitted/null/value semantics.
One possible API could be a generic-like
Omittable<T>wrapper, using PHPDoc generics for static analysis:The GraphQL schema type would still be
String, while the PHP DTO property would preserve whether the field was provided.Resolver code could then look like:
Expected states:
This would be conceptually similar to gqlgen's
Omittable[T]: a value plus a boolean flag indicating whether the field was provided.Reference:
https://github.com/99designs/gqlgen/blob/master/graphql/omittable.go
Expected behavior
For this GraphQL input:
the hydrated DTO should allow resolver code to know that:
For this GraphQL input:
the hydrated DTO should allow resolver code to know that:
For this GraphQL input:
the hydrated DTO should allow resolver code to know that:
Backward compatibility
This should probably be opt-in only.
Existing DTO hydration behavior should remain unchanged unless a field or input object explicitly asks for this behavior.
The opt-in mechanism could be one of:
Omittable;Related issue
This seems related to the broader
ArgumentsTransformerdiscussion:#538
That issue discusses the current limitations of
ArgumentsTransformerand the need to better separate transformation and hydration. This feature request is narrower: preserving GraphQL's omitted/null/value semantics when hydrating input DTOs.Workarounds today
The current workaround is to avoid DTO hydration for PATCH-like mutations and inspect raw arrays manually:
This works, but it means losing typed DTOs in resolvers.
Another workaround is to expose wrapper input objects in the GraphQL schema, but that leaks a server-side implementation concern into the public API and makes the schema less idiomatic.