Skip to content
8 changes: 0 additions & 8 deletions src/HotChocolate/Core/src/Types/Types/InputParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -619,14 +619,6 @@ private static object DeserializeLeaf(
}

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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,11 @@ private static Expression[] CompileAssignParameters<T>(
{
value = CreateOptional(value, field.RuntimeType);
}
else if (parameter.ParameterType.IsValueType
&& System.Nullable.GetUnderlyingType(parameter.ParameterType) == null)
{
value = Expression.Coalesce(value, Expression.Default(parameter.ParameterType));
}

expressions[i] = Expression.Convert(value, parameter.ParameterType);
}
Expand Down Expand Up @@ -243,6 +248,11 @@ private static void CompileSetProperties<T>(
{
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);
Expand Down
135 changes: 135 additions & 0 deletions src/HotChocolate/Core/test/Types.Tests/Types/InputParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,114 @@ public void Force_NonNull_Struct_To_Be_Optional()
Assert.IsType<Test4Input>(runtimeValue).MatchSnapshot();
}

[Fact]
public void Force_NonNull_Struct_Constructor_Parameter_To_Be_Optional()
{
// arrange
var schema = SchemaBuilder.New()
.AddInputObjectType<Test5Input>(d =>
{
d.Field(t => t.Field2).Type<IntType>();
d.Field(t => t.Field3).Type<BooleanType>();
})
.ModifyOptions(o => o.StrictValidation = false)
.Create();

var type = schema.Types.GetType<InputObjectType>("Test5Input");

var context = new Mock<IFeatureProvider>();
context.Setup(t => t.Features).Returns(FeatureCollection.Empty);

var parser = new InputParser(new DefaultTypeConverter());

using var missingFieldsData = JsonDocument.Parse(
"""
{
"field1": "abc"
}
""");

using var explicitNullData = JsonDocument.Parse(
"""
{
"field1": "abc",
"field2": null,
"field3": null
}
""");

// act
var missingFields = Assert.IsType<Test5Input>(
parser.ParseInputValue(
missingFieldsData.RootElement,
type,
context.Object,
Path.Root));
var explicitNull = Assert.IsType<Test5Input>(
parser.ParseInputValue(
explicitNullData.RootElement,
type,
context.Object,
Path.Root));

// assert
Assert.Equal("abc", missingFields.Field1);
Assert.Equal(0, missingFields.Field2);
Assert.False(missingFields.Field3);
Assert.Equal("abc", explicitNull.Field1);
Assert.Equal(0, explicitNull.Field2);
Assert.False(explicitNull.Field3);
}

[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<NonNullType<MyInputType>>())
.Type<StringType>()
.ResolveWith<ResolverArgumentsAccessor>(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<string, IDictionary<string, object?>?> Arguments { get; } = new();

internal string? ResolveWith(IDictionary<string, object?> args)
{
lock (_lock)
{
Arguments[args["string"]!.ToString()!] = args;
}

return "OK";
}
}

public class TestInput
{
public string? Field1 { get; set; }
Expand Down Expand Up @@ -605,4 +713,31 @@ public class Test4Input

public int Field2 { get; set; }
}

public class Test5Input
{
public Test5Input(string field1, int field2, bool field3)
{
Field1 = field1;
Field2 = field2;
Field3 = field3;
}

public string Field1 { get; }

public int Field2 { get; }

public bool Field3 { get; }
}

public class MyInputType : InputObjectType
{
protected override void Configure(IInputObjectTypeDescriptor descriptor)
{
descriptor.Name("MyInput");
descriptor.Field("string").Type<StringType>();
descriptor.Field("int").Type<IntType>();
descriptor.Field("bool").Type<BooleanType>();
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}