diff --git a/src/HotChocolate/Core/src/Types/Configuration/Validation/ObjectTypeValidationRule.cs b/src/HotChocolate/Core/src/Types/Configuration/Validation/ObjectTypeValidationRule.cs index 535a315e51f..56d72295c8a 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/Validation/ObjectTypeValidationRule.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/Validation/ObjectTypeValidationRule.cs @@ -41,6 +41,7 @@ public void Validate( EnsureFieldNamesAreValid(objectType, errors); EnsureInterfacesAreCorrectlyImplemented(objectType, errors); EnsureArgumentDeprecationIsValid(objectType, errors); + EnsureArgumentDefaultValuesAreCompatible(objectType, errors); if (nodeType?.IsAssignableFrom(objectType) == true) { diff --git a/src/HotChocolate/Core/src/Types/Configuration/Validation/TypeValidationHelper.cs b/src/HotChocolate/Core/src/Types/Configuration/Validation/TypeValidationHelper.cs index 483ccfff12c..9127381f8cc 100644 --- a/src/HotChocolate/Core/src/Types/Configuration/Validation/TypeValidationHelper.cs +++ b/src/HotChocolate/Core/src/Types/Configuration/Validation/TypeValidationHelper.cs @@ -5,6 +5,8 @@ namespace HotChocolate.Configuration.Validation; internal static class TypeValidationHelper { + private static readonly InputParser s_inputParser = new(); + public static void EnsureTypeHasFields( IComplexTypeDefinition type, ICollection errors) @@ -45,6 +47,29 @@ public static void EnsureArgumentDeprecationIsValid( } } + public static void EnsureArgumentDefaultValuesAreCompatible( + ObjectType type, + ICollection errors) + { + foreach (var field in type.Fields) + { + foreach (var argument in field.Arguments) + { + if (argument.DefaultValue is not null) + { + try + { + s_inputParser.ParseLiteral(argument.DefaultValue, argument); + } + catch (SerializationException) + { + errors.Add(ArgumentDefaultValueMustBeCompatible(type, field, argument)); + } + } + } + } + } + public static void EnsureArgumentDeprecationIsValid( IDirectiveDefinition type, ICollection errors) diff --git a/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs b/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs index 8044ae7ae8c..d1e74c8efc5 100644 --- a/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs +++ b/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs @@ -772,6 +772,15 @@ internal static string ErrorHelper_AdditionalArgumentNotNullable { } } + /// + /// Looks up a localized string similar to Argument {0} must have a compatible default value.. + /// + internal static string ErrorHelper_ArgumentDefaultValueMustBeCompatible { + get { + return ResourceManager.GetString("ErrorHelper_ArgumentDefaultValueMustBeCompatible", resourceCulture); + } + } + /// /// Looks up a localized string similar to The argument `{0}` of the implemented field `{1}` must be defined. The field `{2}` must include an argument of the same name for every argument defined on the implemented field of the interface type `{3}`.. /// diff --git a/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx b/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx index 18d059343e9..94c032915b7 100644 --- a/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx +++ b/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx @@ -885,6 +885,9 @@ Type: `{0}` Required argument {0} cannot be deprecated. + + Argument {0} must have a compatible default value. + Required input field {0} cannot be deprecated. diff --git a/src/HotChocolate/Core/src/Types/Utilities/ErrorHelper.cs b/src/HotChocolate/Core/src/Types/Utilities/ErrorHelper.cs index df07ef5efbe..33bb40796d2 100644 --- a/src/HotChocolate/Core/src/Types/Utilities/ErrorHelper.cs +++ b/src/HotChocolate/Core/src/Types/Utilities/ErrorHelper.cs @@ -227,6 +227,20 @@ public static ISchemaError RequiredArgumentCannotBeDeprecated( .SetSpecifiedBy(TypeKind.Directive, rfc: 805) .Build(); + public static ISchemaError ArgumentDefaultValueMustBeCompatible( + IComplexTypeDefinition type, + IOutputFieldDefinition field, + IInputValueDefinition argument) + => SchemaErrorBuilder.New() + .SetMessage( + ErrorHelper_ArgumentDefaultValueMustBeCompatible, + argument.Coordinate.ToString()) + .SetType(type) + .SetField(field) + .SetArgument(argument) + .SetSpecifiedBy(type.Kind, rfc: 793) + .Build(); + public static ISchemaError RequiredFieldCannotBeDeprecated( IInputObjectTypeDefinition type, IInputValueDefinition field) diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/ObjectTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/ObjectTypeTests.cs index 3c8eb40745a..2a18de3d00d 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/ObjectTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/ObjectTypeTests.cs @@ -1326,7 +1326,7 @@ public void Argument_Type_IsInferred_From_Parameter() .AddQueryType( t => t .Field(f => f.GetBar(1)) - .Argument("foo", a => a.DefaultValue(null))) + .Argument("foo", a => a.DefaultValue(0))) .Create(); // assert @@ -2320,7 +2320,7 @@ public class QueryWithArgumentDefaults string b = "abc") => null; public string? Field2( - [DefaultValue(null)] string a, + [DefaultValue(null)] string? a, [DefaultValue("abc")] string b) => null; } diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Validation/ObjectTypeValidationRuleTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Validation/ObjectTypeValidationRuleTests.cs index d8124b354aa..070d40ca206 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Validation/ObjectTypeValidationRuleTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Validation/ObjectTypeValidationRuleTests.cs @@ -191,4 +191,28 @@ type Foo { } "); } + + [Fact] + public void AcceptArgumentWithCompatibleDefaultValue() + { + ExpectValid(@" + type Query { stub: String } + + type Foo { + field(arg: Int! = 123): String + } + "); + } + + [Fact] + public void RejectArgumentWithIncompatibleDefaultValue() + { + ExpectError(@" + type Query { stub: String } + + type Foo { + field(arg: Int! = { a: 1 }): String + } + "); + } } diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Validation/__snapshots__/ObjectTypeValidationRuleTests.RejectArgumentWithIncompatibleDefaultValue.snap b/src/HotChocolate/Core/test/Types.Tests/Types/Validation/__snapshots__/ObjectTypeValidationRuleTests.RejectArgumentWithIncompatibleDefaultValue.snap new file mode 100644 index 00000000000..9a95e2debc9 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Validation/__snapshots__/ObjectTypeValidationRuleTests.RejectArgumentWithIncompatibleDefaultValue.snap @@ -0,0 +1,11 @@ +{ + "message": "Argument Foo.field(arg:) must have a compatible default value.", + "type": "Foo", + "extensions": { + "argument": "arg", + "field": "field", + "rfc": "https://github.com/graphql/graphql-spec/pull/793", + "specifiedBy": "https://spec.graphql.org/October2021/#sec-Objects.Type-Validation" + } +} + diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/ObjectTypeTests.Argument_Type_IsInferred_From_Parameter.snap b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/ObjectTypeTests.Argument_Type_IsInferred_From_Parameter.snap index 3b682bb30fa..09e47680409 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/ObjectTypeTests.Argument_Type_IsInferred_From_Parameter.snap +++ b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/ObjectTypeTests.Argument_Type_IsInferred_From_Parameter.snap @@ -3,5 +3,5 @@ schema { } type QueryWithIntArg { - bar(foo: Int!): String! + bar(foo: Int! = 0): String! } diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/ObjectTypeTests.Infer_Argument_Default_Values.snap b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/ObjectTypeTests.Infer_Argument_Default_Values.snap index 3426fd8a88b..aab4548fcd0 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/ObjectTypeTests.Infer_Argument_Default_Values.snap +++ b/src/HotChocolate/Core/test/Types.Tests/Types/__snapshots__/ObjectTypeTests.Infer_Argument_Default_Values.snap @@ -4,5 +4,5 @@ schema { type QueryWithArgumentDefaults { field1(a: String b: String! = "abc"): String - field2(a: String! b: String = "abc"): String + field2(a: String b: String = "abc"): String }