From 4751743d2f91f6689276e6b357bd4144dccda8ce Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Fri, 27 Feb 2026 18:49:21 +0000 Subject: [PATCH 1/2] Fix #7040 enum defaults from underlying values --- .../Descriptors/ArgumentDescriptorBase~1.cs | 33 +++++++++++++++++++ .../Types.Tests/Types/Issue7040ReproTests.cs | 30 +++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/HotChocolate/Core/test/Types.Tests/Types/Issue7040ReproTests.cs diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/ArgumentDescriptorBase~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/ArgumentDescriptorBase~1.cs index 7712256736a..bb5240f1d03 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/ArgumentDescriptorBase~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/ArgumentDescriptorBase~1.cs @@ -150,6 +150,11 @@ public void DefaultValue(object? value) } else { + if (TryCoerceEnumUnderlyingValue(value, out var enumValue)) + { + value = enumValue; + } + var type = Context.TypeInspector.GetType(value.GetType()); Configuration.SetMoreSpecificType(type, TypeContext.Input); Configuration.RuntimeDefaultValue = value; @@ -168,4 +173,32 @@ public void Directive(TDirective directiveInstance) where TDirective /// public void Directive(string name, params ArgumentNode[] arguments) => Configuration.AddDirective(name, arguments); + + private bool TryCoerceEnumUnderlyingValue(object value, out object enumValue) + { + enumValue = default!; + + if (Configuration.Type is not ExtendedTypeReference typeReference) + { + return false; + } + + var clrType = Nullable.GetUnderlyingType(typeReference.Type.Source) + ?? typeReference.Type.Source; + + if (!clrType.IsEnum) + { + return false; + } + + var underlyingType = Enum.GetUnderlyingType(clrType); + + if (value.GetType() != underlyingType) + { + return false; + } + + enumValue = Enum.ToObject(clrType, value); + return true; + } } diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Issue7040ReproTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Issue7040ReproTests.cs new file mode 100644 index 00000000000..fb0f354d971 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Issue7040ReproTests.cs @@ -0,0 +1,30 @@ +namespace HotChocolate.Types; + +public class Issue7040ReproTests +{ + [Fact] + public void Enum_DefaultValueAttribute_With_Integer_Default_Does_Not_Downgrade_To_Int() + { + var schema = SchemaBuilder.New() + .AddInputObjectType() + .ModifyOptions(o => o.StrictValidation = false) + .Create(); + + var sdl = schema.ToString(); + + Assert.Contains("enum: Issue7040Enum! = VALUE1", sdl, StringComparison.Ordinal); + Assert.DoesNotContain("enum: Int! = 0", sdl, StringComparison.Ordinal); + } + + public class InputWithEnumIntDefault + { + [DefaultValue(0)] + public Issue7040Enum Enum { get; set; } + } + + public enum Issue7040Enum + { + Value1 = 0, + Value2 = 1 + } +} From 64e03698e995447865f5db38ef98e861c805ee1f Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Mon, 2 Mar 2026 12:47:37 +0000 Subject: [PATCH 2/2] polish --- .../Descriptors/ArgumentDescriptorBase~1.cs | 20 ++++++++----------- .../Contracts/IArgumentDescriptor.cs | 10 ++++------ .../Types.Tests/Types/Issue7040ReproTests.cs | 14 ++++++------- 3 files changed, 19 insertions(+), 25 deletions(-) diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/ArgumentDescriptorBase~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/ArgumentDescriptorBase~1.cs index bb5240f1d03..ee4b8b40131 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/ArgumentDescriptorBase~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/ArgumentDescriptorBase~1.cs @@ -39,21 +39,15 @@ protected void Deprecated(string? reason) /// protected void Deprecated() - { - Configuration.DeprecationReason = DirectiveNames.Deprecated.Arguments.DefaultReason; - } + => Configuration.DeprecationReason = DirectiveNames.Deprecated.Arguments.DefaultReason; - /// - protected void Description(string value) - { - Configuration.Description = value; - } + /// + protected void Description(string? value) + => Configuration.Description = value; /// public void Type() where TInputType : IInputType - { - Type(typeof(TInputType)); - } + => Type(typeof(TInputType)); /// /// Sets the type of the argument @@ -71,6 +65,8 @@ public void Type() where TInputType : IInputType /// public void Type(Type type) { + ArgumentNullException.ThrowIfNull(type); + var typeInfo = Context.TypeInspector.CreateTypeInfo(type); if (typeInfo.IsSchemaType && !typeInfo.IsInputType()) @@ -133,7 +129,7 @@ public void Type(ITypeNode typeNode) Configuration.SetMoreSpecificType(typeNode, TypeContext.Input); } - /// + /// public void DefaultValue(IValueNode? value) { Configuration.DefaultValue = value ?? NullValueNode.Default; diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/IArgumentDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/IArgumentDescriptor.cs index 9ecf394194e..e2a7a64a814 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/IArgumentDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/IArgumentDescriptor.cs @@ -1,5 +1,3 @@ -#nullable disable - using HotChocolate.Language; using HotChocolate.Types.Descriptors.Configurations; @@ -26,7 +24,7 @@ public interface IArgumentDescriptor /// /// /// - IArgumentDescriptor Deprecated(string reason); + IArgumentDescriptor Deprecated(string? reason); /// /// Marks the argument as deprecated @@ -65,7 +63,7 @@ public interface IArgumentDescriptor /// /// /// The description - IArgumentDescriptor Description(string value); + IArgumentDescriptor Description(string? value); /// /// Sets the type of the argument @@ -149,7 +147,7 @@ IArgumentDescriptor Type(TInputType inputType) /// /// /// - IArgumentDescriptor DefaultValue(IValueNode value); + IArgumentDescriptor DefaultValue(IValueNode? value); /// /// Sets the default value of this argument @@ -166,7 +164,7 @@ IArgumentDescriptor Type(TInputType inputType) /// /// /// - IArgumentDescriptor DefaultValue(object value); + IArgumentDescriptor DefaultValue(object? value); /// /// Sets a directive on the argument diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Issue7040ReproTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Issue7040ReproTests.cs index fb0f354d971..1fe6d11aab1 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Issue7040ReproTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Issue7040ReproTests.cs @@ -5,19 +5,19 @@ public class Issue7040ReproTests [Fact] public void Enum_DefaultValueAttribute_With_Integer_Default_Does_Not_Downgrade_To_Int() { - var schema = SchemaBuilder.New() + SchemaBuilder.New() .AddInputObjectType() .ModifyOptions(o => o.StrictValidation = false) - .Create(); - - var sdl = schema.ToString(); - - Assert.Contains("enum: Issue7040Enum! = VALUE1", sdl, StringComparison.Ordinal); - Assert.DoesNotContain("enum: Int! = 0", sdl, StringComparison.Ordinal); + .Create() + .MatchSnapshot(); } public class InputWithEnumIntDefault { + // The F# compiler boxes enum values as their underlying type when passed + // to attribute constructors that accept object. So [] + // in F# arrives at runtime as [DefaultValue(0)] rather than [DefaultValue(MyEnum.Value1)]. + // We use the integer literal here to reproduce that behavior in C#. [DefaultValue(0)] public Issue7040Enum Enum { get; set; } }