diff --git a/src/HotChocolate/Core/src/Types/Types/InputParser.cs b/src/HotChocolate/Core/src/Types/Types/InputParser.cs index 3067f926142..411ef59ccb6 100644 --- a/src/HotChocolate/Core/src/Types/Types/InputParser.cs +++ b/src/HotChocolate/Core/src/Types/Types/InputParser.cs @@ -614,14 +614,6 @@ private object DeserializeObject(object resultValue, InputObjectType type, Path } object? value = null; - - // if the type is nullable but the runtime type is a non-nullable value - // we will create a default instance and assign that instead. - if (field.RuntimeType.IsValueType) - { - value = Activator.CreateInstance(field.RuntimeType); - } - return field.IsOptional ? new Optional(value, false) : value; diff --git a/src/HotChocolate/Core/src/Types/Utilities/Serialization/InputObjectCompiler.cs b/src/HotChocolate/Core/src/Types/Utilities/Serialization/InputObjectCompiler.cs index 1cccf1289a1..6678e928338 100644 --- a/src/HotChocolate/Core/src/Types/Utilities/Serialization/InputObjectCompiler.cs +++ b/src/HotChocolate/Core/src/Types/Utilities/Serialization/InputObjectCompiler.cs @@ -243,6 +243,11 @@ private static void CompileSetProperties( { value = CreateOptional(value, field.RuntimeType); } + else if (field.Property.PropertyType.IsValueType + && System.Nullable.GetUnderlyingType(field.Property.PropertyType) == null) + { + value = Expression.Coalesce(value, Expression.Default(field.Property.PropertyType)); + } value = Expression.Convert(value, field.Property.PropertyType); Expression setPropertyValue = Expression.Call(instance, setter, value); diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/InputParserTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/InputParserTests.cs index e0523c411cd..21660f013f6 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/InputParserTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/InputParserTests.cs @@ -493,6 +493,55 @@ public void Force_NonNull_Struct_To_Be_Optional() Assert.IsType(runtimeValue).MatchSnapshot(); } + [Fact] + public async Task Integration_CodeFirst_InputObjectNoDefaultValue_NoRuntimeTypeDefaultValueIsInitialized() + { + // arrange + var resolverArgumentsAccessor = new ResolverArgumentsAccessor(); + var executor = await new ServiceCollection() + .AddSingleton(resolverArgumentsAccessor) + .AddGraphQL() + .AddQueryType(x => x.Field("foo") + .Argument("args", a => a.Type>()) + .Type() + .ResolveWith(r => r.ResolveWith(default!))) + .BuildRequestExecutorAsync(); + + // act + var query = + OperationRequest.FromSourceText( + """ + { + a: foo(args: { string: "allSet" int: 1 bool: true }) + b: foo(args: { string: "noneSet" }) + c: foo(args: { string: "intExplicitlyNull" int: null }) + d: foo(args: { string: "boolExplicitlyNull" bool: null }) + e: foo(args: { string: "intSetBoolNull" int: 1 bool: null }) + f: foo(args: { string: "boolSetIntNull" int: null bool: true }) + } + """); + await executor.ExecuteAsync(query, CancellationToken.None); + + // assert + resolverArgumentsAccessor.Arguments.MatchSnapshot(); + } + + private class ResolverArgumentsAccessor + { + private readonly object _lock = new(); + internal SortedDictionary?> Arguments { get; } = new(); + + internal string? ResolveWith(IDictionary args) + { + lock (_lock) + { + Arguments[args["string"]!.ToString()!] = args; + } + + return "OK"; + } + } + public class TestInput { public string? Field1 { get; set; } @@ -564,4 +613,15 @@ public class Test4Input public int Field2 { get; set; } } + + public class MyInputType : InputObjectType + { + protected override void Configure(IInputObjectTypeDescriptor descriptor) + { + descriptor.Name("MyInput"); + descriptor.Field("string").Type(); + descriptor.Field("int").Type(); + descriptor.Field("bool").Type(); + } + } } diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputParserTests.Integration_CodeFirst_InputObjectNoDefaultValue_NoRuntimeTypeDefaultValueIsInitialized.snap b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputParserTests.Integration_CodeFirst_InputObjectNoDefaultValue_NoRuntimeTypeDefaultValueIsInitialized.snap new file mode 100644 index 00000000000..8d706adf995 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/InputParserTests.Integration_CodeFirst_InputObjectNoDefaultValue_NoRuntimeTypeDefaultValueIsInitialized.snap @@ -0,0 +1,32 @@ +{ + "allSet": { + "string": "allSet", + "int": 1, + "bool": true + }, + "boolExplicitlyNull": { + "string": "boolExplicitlyNull", + "int": null, + "bool": null + }, + "boolSetIntNull": { + "string": "boolSetIntNull", + "int": null, + "bool": true + }, + "intExplicitlyNull": { + "string": "intExplicitlyNull", + "int": null, + "bool": null + }, + "intSetBoolNull": { + "string": "intSetBoolNull", + "int": 1, + "bool": null + }, + "noneSet": { + "string": "noneSet", + "int": null, + "bool": null + } +}