From c7eea39b1731e3fef32f54c88d59793b163887a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:48:52 +0000 Subject: [PATCH 1/3] Initial plan From defeda11933a335b06d6d8bcfed8f156bcb7de2a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:53:14 +0000 Subject: [PATCH 2/3] Add OneOf inhabitability validation Co-authored-by: andimarek <1706744+andimarek@users.noreply.github.com> --- .../validation/OneOfInputObjectRules.java | 44 ++++ .../validation/SchemaValidationErrorType.java | 1 + .../OneOfInputObjectRulesTest.groovy | 239 ++++++++++++++++++ 3 files changed, 284 insertions(+) diff --git a/src/main/java/graphql/schema/validation/OneOfInputObjectRules.java b/src/main/java/graphql/schema/validation/OneOfInputObjectRules.java index 87e39fc321..b41a7e570b 100644 --- a/src/main/java/graphql/schema/validation/OneOfInputObjectRules.java +++ b/src/main/java/graphql/schema/validation/OneOfInputObjectRules.java @@ -4,11 +4,16 @@ import graphql.schema.GraphQLInputObjectField; import graphql.schema.GraphQLInputObjectType; import graphql.schema.GraphQLSchemaElement; +import graphql.schema.GraphQLType; import graphql.schema.GraphQLTypeUtil; import graphql.schema.GraphQLTypeVisitorStub; +import graphql.schema.GraphQLUnmodifiedType; import graphql.util.TraversalControl; import graphql.util.TraverserContext; +import java.util.LinkedHashSet; +import java.util.Set; + import static java.lang.String.format; /* @@ -19,6 +24,45 @@ @ExperimentalApi public class OneOfInputObjectRules extends GraphQLTypeVisitorStub { + @Override + public TraversalControl visitGraphQLInputObjectType(GraphQLInputObjectType inputObjectType, TraverserContext context) { + if (!inputObjectType.isOneOf()) { + return TraversalControl.CONTINUE; + } + SchemaValidationErrorCollector errorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class); + if (!canBeProvidedAFiniteValue(inputObjectType, new LinkedHashSet<>())) { + String message = format("OneOf Input Object %s must be inhabited but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.", inputObjectType.getName()); + errorCollector.addError(new SchemaValidationError(SchemaValidationErrorType.OneOfNotInhabited, message)); + } + return TraversalControl.CONTINUE; + } + + private boolean canBeProvidedAFiniteValue(GraphQLInputObjectType oneOfInputObject, Set visited) { + if (visited.contains(oneOfInputObject)) { + return false; + } + Set nextVisited = new LinkedHashSet<>(visited); + nextVisited.add(oneOfInputObject); + for (GraphQLInputObjectField field : oneOfInputObject.getFieldDefinitions()) { + GraphQLType fieldType = field.getType(); + if (GraphQLTypeUtil.isList(fieldType)) { + return true; + } + GraphQLUnmodifiedType namedFieldType = GraphQLTypeUtil.unwrapAll(fieldType); + if (!(namedFieldType instanceof GraphQLInputObjectType)) { + return true; + } + GraphQLInputObjectType inputFieldType = (GraphQLInputObjectType) namedFieldType; + if (!inputFieldType.isOneOf()) { + return true; + } + if (canBeProvidedAFiniteValue(inputFieldType, nextVisited)) { + return true; + } + } + return false; + } + @Override public TraversalControl visitGraphQLInputObjectField(GraphQLInputObjectField inputObjectField, TraverserContext context) { GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) context.getParentNode(); diff --git a/src/main/java/graphql/schema/validation/SchemaValidationErrorType.java b/src/main/java/graphql/schema/validation/SchemaValidationErrorType.java index b8392b18d7..b1caecc8e4 100644 --- a/src/main/java/graphql/schema/validation/SchemaValidationErrorType.java +++ b/src/main/java/graphql/schema/validation/SchemaValidationErrorType.java @@ -22,6 +22,7 @@ public enum SchemaValidationErrorType implements SchemaValidationErrorClassifica InputTypeUsedInOutputTypeContext, OneOfDefaultValueOnField, OneOfNonNullableField, + OneOfNotInhabited, RequiredInputFieldCannotBeDeprecated, RequiredFieldArgumentCannotBeDeprecated, RequiredDirectiveArgumentCannotBeDeprecated diff --git a/src/test/groovy/graphql/schema/validation/OneOfInputObjectRulesTest.groovy b/src/test/groovy/graphql/schema/validation/OneOfInputObjectRulesTest.groovy index 813e54672b..5b2957da99 100644 --- a/src/test/groovy/graphql/schema/validation/OneOfInputObjectRulesTest.groovy +++ b/src/test/groovy/graphql/schema/validation/OneOfInputObjectRulesTest.groovy @@ -33,4 +33,243 @@ class OneOfInputObjectRulesTest extends Specification { schemaProblem.errors[1].description == "OneOf input field OneOfInputType.badDefaulted cannot have a default value." schemaProblem.errors[1].classification == SchemaValidationErrorType.OneOfDefaultValueOnField } + + def "oneOf with scalar fields is inhabited"() { + def sdl = """ + type Query { + f(arg : A) : String + } + + input A @oneOf { + a : String + b : Int + } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "oneOf with enum field is inhabited"() { + def sdl = """ + type Query { + f(arg : A) : String + } + + enum Color { + RED + GREEN + BLUE + } + + input A @oneOf { + a : Color + } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "oneOf with list field is inhabited"() { + def sdl = """ + type Query { + f(arg : A) : String + } + + input A @oneOf { + a : [A] + } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "oneOf referencing non-oneOf input is inhabited"() { + def sdl = """ + type Query { + f(arg : A) : String + } + + input RegularInput { + x : String + } + + input A @oneOf { + a : RegularInput + } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "oneOf with escape field is inhabited"() { + def sdl = """ + type Query { + f(arg : A) : String + } + + input A @oneOf { + b : B + escape : String + } + + input B @oneOf { + a : A + } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "mutually referencing oneOf types with scalar escape is inhabited"() { + def sdl = """ + type Query { + f(arg : A) : String + } + + input A @oneOf { + b : B + } + + input B @oneOf { + a : A + escape : Int + } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "oneOf referencing non-oneOf with back-reference is inhabited"() { + def sdl = """ + type Query { + f(arg : A) : String + } + + input A @oneOf { + b : RegularInput + } + + input RegularInput { + back : A + } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "multiple fields with chained oneOf escape is inhabited"() { + def sdl = """ + type Query { + f(arg : A) : String + } + + input A @oneOf { + b : B + c : C + } + + input B @oneOf { + a : A + } + + input C @oneOf { + a : A + escape : String + } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "single oneOf self-reference cycle is not inhabited"() { + def sdl = """ + type Query { + f(arg : A) : String + } + + input A @oneOf { + self : A + } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.errors.size() == 1 + schemaProblem.errors[0].classification == SchemaValidationErrorType.OneOfNotInhabited + } + + def "multiple oneOf types forming cycle are not inhabited"() { + def sdl = """ + type Query { + f(arg : A) : String + } + + input A @oneOf { + b : B + } + + input B @oneOf { + c : C + } + + input C @oneOf { + a : A + } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.errors.size() == 3 + schemaProblem.errors[0].classification == SchemaValidationErrorType.OneOfNotInhabited + schemaProblem.errors[1].classification == SchemaValidationErrorType.OneOfNotInhabited + schemaProblem.errors[2].classification == SchemaValidationErrorType.OneOfNotInhabited + } } From 4890f7f548364a477938670d54427bbcc3e0f033 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 02:05:02 +0000 Subject: [PATCH 3/3] Inline OneOf validation rules into TypeAndFieldRule Co-authored-by: andimarek <1706744+andimarek@users.noreply.github.com> --- .../validation/OneOfInputObjectRules.java | 85 ------------------- .../schema/validation/SchemaValidator.java | 1 - .../schema/validation/TypeAndFieldRule.java | 57 +++++++++++++ .../validation/SchemaValidatorTest.groovy | 5 +- 4 files changed, 59 insertions(+), 89 deletions(-) delete mode 100644 src/main/java/graphql/schema/validation/OneOfInputObjectRules.java diff --git a/src/main/java/graphql/schema/validation/OneOfInputObjectRules.java b/src/main/java/graphql/schema/validation/OneOfInputObjectRules.java deleted file mode 100644 index b41a7e570b..0000000000 --- a/src/main/java/graphql/schema/validation/OneOfInputObjectRules.java +++ /dev/null @@ -1,85 +0,0 @@ -package graphql.schema.validation; - -import graphql.ExperimentalApi; -import graphql.schema.GraphQLInputObjectField; -import graphql.schema.GraphQLInputObjectType; -import graphql.schema.GraphQLSchemaElement; -import graphql.schema.GraphQLType; -import graphql.schema.GraphQLTypeUtil; -import graphql.schema.GraphQLTypeVisitorStub; -import graphql.schema.GraphQLUnmodifiedType; -import graphql.util.TraversalControl; -import graphql.util.TraverserContext; - -import java.util.LinkedHashSet; -import java.util.Set; - -import static java.lang.String.format; - -/* - * Spec: If the Input Object is a OneOf Input Object then: - * The type of the input field must be nullable. - * The input field must not have a default value. - */ -@ExperimentalApi -public class OneOfInputObjectRules extends GraphQLTypeVisitorStub { - - @Override - public TraversalControl visitGraphQLInputObjectType(GraphQLInputObjectType inputObjectType, TraverserContext context) { - if (!inputObjectType.isOneOf()) { - return TraversalControl.CONTINUE; - } - SchemaValidationErrorCollector errorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class); - if (!canBeProvidedAFiniteValue(inputObjectType, new LinkedHashSet<>())) { - String message = format("OneOf Input Object %s must be inhabited but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.", inputObjectType.getName()); - errorCollector.addError(new SchemaValidationError(SchemaValidationErrorType.OneOfNotInhabited, message)); - } - return TraversalControl.CONTINUE; - } - - private boolean canBeProvidedAFiniteValue(GraphQLInputObjectType oneOfInputObject, Set visited) { - if (visited.contains(oneOfInputObject)) { - return false; - } - Set nextVisited = new LinkedHashSet<>(visited); - nextVisited.add(oneOfInputObject); - for (GraphQLInputObjectField field : oneOfInputObject.getFieldDefinitions()) { - GraphQLType fieldType = field.getType(); - if (GraphQLTypeUtil.isList(fieldType)) { - return true; - } - GraphQLUnmodifiedType namedFieldType = GraphQLTypeUtil.unwrapAll(fieldType); - if (!(namedFieldType instanceof GraphQLInputObjectType)) { - return true; - } - GraphQLInputObjectType inputFieldType = (GraphQLInputObjectType) namedFieldType; - if (!inputFieldType.isOneOf()) { - return true; - } - if (canBeProvidedAFiniteValue(inputFieldType, nextVisited)) { - return true; - } - } - return false; - } - - @Override - public TraversalControl visitGraphQLInputObjectField(GraphQLInputObjectField inputObjectField, TraverserContext context) { - GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) context.getParentNode(); - if (!inputObjectType.isOneOf()) { - return TraversalControl.CONTINUE; - } - SchemaValidationErrorCollector errorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class); - // error messages take from the reference implementation - if (inputObjectField.hasSetDefaultValue()) { - String message = format("OneOf input field %s.%s cannot have a default value.", inputObjectType.getName(), inputObjectField.getName()); - errorCollector.addError(new SchemaValidationError(SchemaValidationErrorType.OneOfDefaultValueOnField, message)); - } - - if (GraphQLTypeUtil.isNonNull(inputObjectField.getType())) { - String message = format("OneOf input field %s.%s must be nullable.", inputObjectType.getName(), inputObjectField.getName()); - errorCollector.addError(new SchemaValidationError(SchemaValidationErrorType.OneOfNonNullableField, message)); - } - return TraversalControl.CONTINUE; - } -} diff --git a/src/main/java/graphql/schema/validation/SchemaValidator.java b/src/main/java/graphql/schema/validation/SchemaValidator.java index 1f676fb77e..ea3700e3c6 100644 --- a/src/main/java/graphql/schema/validation/SchemaValidator.java +++ b/src/main/java/graphql/schema/validation/SchemaValidator.java @@ -25,7 +25,6 @@ public SchemaValidator() { rules.add(new AppliedDirectivesAreValid()); rules.add(new AppliedDirectiveArgumentsAreValid()); rules.add(new InputAndOutputTypesUsedAppropriately()); - rules.add(new OneOfInputObjectRules()); rules.add(new DeprecatedInputObjectAndArgumentsAreValid()); } diff --git a/src/main/java/graphql/schema/validation/TypeAndFieldRule.java b/src/main/java/graphql/schema/validation/TypeAndFieldRule.java index 84a768479d..c5e2dc4e90 100644 --- a/src/main/java/graphql/schema/validation/TypeAndFieldRule.java +++ b/src/main/java/graphql/schema/validation/TypeAndFieldRule.java @@ -19,11 +19,13 @@ import graphql.schema.GraphQLTypeUtil; import graphql.schema.GraphQLTypeVisitorStub; import graphql.schema.GraphQLUnionType; +import graphql.schema.GraphQLUnmodifiedType; import graphql.util.FpKit; import graphql.util.TraversalControl; import graphql.util.TraverserContext; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.function.Predicate; @@ -91,6 +93,35 @@ public TraversalControl visitGraphQLInputObjectType(GraphQLInputObjectType type, } SchemaValidationErrorCollector errorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class); validateInputObject((GraphQLInputObjectType) type, errorCollector); + + // OneOf validation: check if OneOf input types are inhabited + if (type.isOneOf()) { + if (!canBeProvidedAFiniteValue(type, new LinkedHashSet<>())) { + String message = String.format("OneOf Input Object %s must be inhabited but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.", type.getName()); + errorCollector.addError(new SchemaValidationError(SchemaValidationErrorType.OneOfNotInhabited, message)); + } + } + + return TraversalControl.CONTINUE; + } + + @Override + public TraversalControl visitGraphQLInputObjectField(GraphQLInputObjectField inputObjectField, TraverserContext context) { + GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) context.getParentNode(); + if (!inputObjectType.isOneOf()) { + return TraversalControl.CONTINUE; + } + SchemaValidationErrorCollector errorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class); + // OneOf validation: error messages taken from the reference implementation + if (inputObjectField.hasSetDefaultValue()) { + String message = String.format("OneOf input field %s.%s cannot have a default value.", inputObjectType.getName(), inputObjectField.getName()); + errorCollector.addError(new SchemaValidationError(SchemaValidationErrorType.OneOfDefaultValueOnField, message)); + } + + if (GraphQLTypeUtil.isNonNull(inputObjectField.getType())) { + String message = String.format("OneOf input field %s.%s must be nullable.", inputObjectType.getName(), inputObjectField.getName()); + errorCollector.addError(new SchemaValidationError(SchemaValidationErrorType.OneOfNonNullableField, message)); + } return TraversalControl.CONTINUE; } @@ -121,6 +152,32 @@ private void validateInputObject(GraphQLInputObjectType type, SchemaValidationEr } } + private boolean canBeProvidedAFiniteValue(GraphQLInputObjectType oneOfInputObject, Set visited) { + if (visited.contains(oneOfInputObject)) { + return false; + } + Set nextVisited = new LinkedHashSet<>(visited); + nextVisited.add(oneOfInputObject); + for (GraphQLInputObjectField field : oneOfInputObject.getFieldDefinitions()) { + GraphQLType fieldType = field.getType(); + if (GraphQLTypeUtil.isList(fieldType)) { + return true; + } + GraphQLUnmodifiedType namedFieldType = GraphQLTypeUtil.unwrapAll(fieldType); + if (!(namedFieldType instanceof GraphQLInputObjectType)) { + return true; + } + GraphQLInputObjectType inputFieldType = (GraphQLInputObjectType) namedFieldType; + if (!inputFieldType.isOneOf()) { + return true; + } + if (canBeProvidedAFiniteValue(inputFieldType, nextVisited)) { + return true; + } + } + return false; + } + private void validateUnion(GraphQLUnionType type, SchemaValidationErrorCollector errorCollector) { List memberTypes = type.getTypes(); if (memberTypes.size() == 0) { diff --git a/src/test/groovy/graphql/schema/validation/SchemaValidatorTest.groovy b/src/test/groovy/graphql/schema/validation/SchemaValidatorTest.groovy index 706542df8d..5909794071 100644 --- a/src/test/groovy/graphql/schema/validation/SchemaValidatorTest.groovy +++ b/src/test/groovy/graphql/schema/validation/SchemaValidatorTest.groovy @@ -11,7 +11,7 @@ class SchemaValidatorTest extends Specification { def validator = new SchemaValidator() def rules = validator.rules then: - rules.size() == 9 + rules.size() == 8 rules[0] instanceof NoUnbrokenInputCycles rules[1] instanceof TypesImplementInterfaces rules[2] instanceof TypeAndFieldRule @@ -19,7 +19,6 @@ class SchemaValidatorTest extends Specification { rules[4] instanceof AppliedDirectivesAreValid rules[5] instanceof AppliedDirectiveArgumentsAreValid rules[6] instanceof InputAndOutputTypesUsedAppropriately - rules[7] instanceof OneOfInputObjectRules - rules[8] instanceof DeprecatedInputObjectAndArgumentsAreValid + rules[7] instanceof DeprecatedInputObjectAndArgumentsAreValid } }