diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs
index 0d41e0883a3..db11ba87062 100644
--- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs
@@ -8,6 +8,7 @@ public static class LogEntryCodes
public const string ExternalOnInterface = "EXTERNAL_ON_INTERFACE";
public const string ExternalUnused = "EXTERNAL_UNUSED";
public const string InputFieldDefaultMismatch = "INPUT_FIELD_DEFAULT_MISMATCH";
+ public const string InputFieldTypesNotMergeable = "INPUT_FIELD_TYPES_NOT_MERGEABLE";
public const string KeyDirectiveInFieldsArg = "KEY_DIRECTIVE_IN_FIELDS_ARG";
public const string KeyFieldsHasArgs = "KEY_FIELDS_HAS_ARGS";
public const string KeyFieldsSelectInvalidType = "KEY_FIELDS_SELECT_INVALID_TYPE";
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs
index 618a169d442..b97f8f61b08 100644
--- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs
@@ -187,6 +187,27 @@ public static LogEntry InputFieldDefaultMismatch(
schemaA);
}
+ public static LogEntry InputFieldTypesNotMergeable(
+ InputFieldDefinition field,
+ string typeName,
+ SchemaDefinition schemaA,
+ SchemaDefinition schemaB)
+ {
+ var coordinate = new SchemaCoordinate(typeName, field.Name);
+
+ return new LogEntry(
+ string.Format(
+ LogEntryHelper_InputFieldTypesNotMergeable,
+ coordinate,
+ schemaA.Name,
+ schemaB.Name),
+ LogEntryCodes.InputFieldTypesNotMergeable,
+ LogSeverity.Error,
+ coordinate,
+ field,
+ schemaA);
+ }
+
public static LogEntry KeyDirectiveInFieldsArgument(
string entityTypeName,
Directive keyDirective,
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/InputFieldTypesMergeableRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/InputFieldTypesMergeableRule.cs
new file mode 100644
index 00000000000..d7e5b12d4f3
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/InputFieldTypesMergeableRule.cs
@@ -0,0 +1,46 @@
+using HotChocolate.Fusion.Events;
+using static HotChocolate.Fusion.Logging.LogEntryHelper;
+
+namespace HotChocolate.Fusion.PreMergeValidation.Rules;
+
+///
+///
+/// The input fields of input objects with the same name must be mergeable. This rule ensures that
+/// input objects with the same name in different source schemas have fields that can be merged
+/// consistently without conflicts.
+///
+///
+/// Input fields are considered mergeable when they share the same name and have compatible types.
+/// The compatibility of types is determined by their structure (e.g., lists), excluding
+/// nullability. Mergeable input fields with different nullability are considered mergeable,
+/// and the resulting merged field will be the most permissive of the two.
+///
+///
+///
+/// Specification
+///
+internal sealed class InputFieldTypesMergeableRule : IEventHandler
+{
+ public void Handle(InputFieldGroupEvent @event, CompositionContext context)
+ {
+ var (_, fieldGroup, typeName) = @event;
+
+ for (var i = 0; i < fieldGroup.Length - 1; i++)
+ {
+ var fieldInfoA = fieldGroup[i];
+ var fieldInfoB = fieldGroup[i + 1];
+ var typeA = fieldInfoA.Field.Type;
+ var typeB = fieldInfoB.Field.Type;
+
+ if (!ValidationHelper.SameTypeShape(typeA, typeB))
+ {
+ context.Log.Write(
+ InputFieldTypesNotMergeable(
+ fieldInfoA.Field,
+ typeName,
+ fieldInfoA.Schema,
+ fieldInfoB.Schema));
+ }
+ }
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs
index cd4eba83903..a4300a30716 100644
--- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs
@@ -158,6 +158,15 @@ internal static string LogEntryHelper_InputFieldDefaultMismatch {
}
}
+ ///
+ /// Looks up a localized string similar to The input field '{0}' has a different type shape in schema '{1}' than it does in schema '{2}'..
+ ///
+ internal static string LogEntryHelper_InputFieldTypesNotMergeable {
+ get {
+ return ResourceManager.GetString("LogEntryHelper_InputFieldTypesNotMergeable", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to A @key directive on type '{0}' in schema '{1}' references field '{2}', which must not include directive applications..
///
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx
index f41a27f757f..5daa07cb788 100644
--- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx
@@ -51,6 +51,9 @@
The default value '{0}' of input field '{1}' in schema '{2}' differs from the default value of '{3}' in schema '{4}'.
+
+ The input field '{0}' has a different type shape in schema '{1}' than it does in schema '{2}'.
+
A @key directive on type '{0}' in schema '{1}' references field '{2}', which must not include directive applications.
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs
index 1b4d745c70e..0079a5f0c20 100644
--- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs
@@ -52,6 +52,7 @@ private CompositionResult MergeSchemaDefinitions(CompositionCo
new ExternalOnInterfaceRule(),
new ExternalUnusedRule(),
new InputFieldDefaultMismatchRule(),
+ new InputFieldTypesMergeableRule(),
new KeyDirectiveInFieldsArgumentRule(),
new KeyFieldsHasArgumentsRule(),
new KeyFieldsSelectInvalidTypeRule(),
diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/InputFieldTypesMergeableRuleTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/InputFieldTypesMergeableRuleTests.cs
new file mode 100644
index 00000000000..0321689f437
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/InputFieldTypesMergeableRuleTests.cs
@@ -0,0 +1,152 @@
+using HotChocolate.Fusion.Logging;
+using HotChocolate.Fusion.PreMergeValidation;
+using HotChocolate.Fusion.PreMergeValidation.Rules;
+
+namespace HotChocolate.Composition.PreMergeValidation.Rules;
+
+public sealed class InputFieldTypesMergeableRuleTests : CompositionTestBase
+{
+ private readonly PreMergeValidator _preMergeValidator =
+ new([new InputFieldTypesMergeableRule()]);
+
+ [Theory]
+ [MemberData(nameof(ValidExamplesData))]
+ public void Examples_Valid(string[] sdl)
+ {
+ // arrange
+ var context = CreateCompositionContext(sdl);
+
+ // act
+ var result = _preMergeValidator.Validate(context);
+
+ // assert
+ Assert.True(result.IsSuccess);
+ Assert.True(context.Log.IsEmpty);
+ }
+
+ [Theory]
+ [MemberData(nameof(InvalidExamplesData))]
+ public void Examples_Invalid(string[] sdl, string[] errorMessages)
+ {
+ // arrange
+ var context = CreateCompositionContext(sdl);
+
+ // act
+ var result = _preMergeValidator.Validate(context);
+
+ // assert
+ Assert.True(result.IsFailure);
+ Assert.Equal(errorMessages, context.Log.Select(e => e.Message).ToArray());
+ Assert.True(context.Log.All(e => e.Code == "INPUT_FIELD_TYPES_NOT_MERGEABLE"));
+ Assert.True(context.Log.All(e => e.Severity == LogSeverity.Error));
+ }
+
+ public static TheoryData ValidExamplesData()
+ {
+ return new TheoryData
+ {
+ // In this example, the field "name" in "AuthorInput" has compatible types across source
+ // schemas, making them mergeable.
+ {
+ [
+ """
+ input AuthorInput {
+ name: String!
+ }
+ """,
+ """
+ input AuthorInput {
+ name: String
+ }
+ """
+ ]
+ },
+ // The following example shows that fields are mergeable if they have different
+ // nullability but the named type is the same and the list structure is the same.
+ {
+ [
+ """
+ input AuthorInput {
+ tags: [String!]
+ }
+ """,
+ """
+ input AuthorInput {
+ tags: [String]!
+ }
+ """,
+ """
+ input AuthorInput {
+ tags: [String]
+ }
+ """
+ ]
+ },
+ // Multiple input fields.
+ {
+ [
+ """
+ input AuthorInput {
+ name: String!
+ tags: [String!]
+ birthdate: DateTime
+ }
+ """,
+ """
+ input AuthorInput {
+ name: String
+ tags: [String]!
+ birthdate: DateTime!
+ }
+ """
+ ]
+ },
+ };
+ }
+
+ public static TheoryData InvalidExamplesData()
+ {
+ return new TheoryData
+ {
+ // In this example, the field "birthdate" on "AuthorInput" is not mergeable as the field
+ // has different named types ("String" and "DateTime") across source schemas.
+ {
+ [
+ """
+ input AuthorInput {
+ birthdate: String!
+ }
+ """,
+ """
+ input AuthorInput {
+ birthdate: DateTime!
+ }
+ """
+ ],
+ [
+ "Input field 'AuthorInput.birthdate' has a different type shape in schema " +
+ "'A' than it does in schema 'B'."
+ ]
+ },
+ // List versus non-list.
+ {
+ [
+ """
+ input AuthorInput {
+ birthdate: String!
+ }
+ """,
+ """
+ input AuthorInput {
+ birthdate: [String!]
+ }
+ """
+ ],
+ [
+ "Input field 'AuthorInput.birthdate' has a different type shape in schema " +
+ "'A' than it does in schema 'B'."
+ ]
+ }
+ };
+ }
+}