Skip to content

Add OneOf Inhabitability validation#4250

Closed
Copilot wants to merge 3 commits intovalidation-refactorfrom
copilot/validate-oneof-inhabited-types
Closed

Add OneOf Inhabitability validation#4250
Copilot wants to merge 3 commits intovalidation-refactorfrom
copilot/validate-oneof-inhabited-types

Conversation

Copy link
Contributor

Copilot AI commented Feb 19, 2026

Implements validation to detect uninhabited OneOf input types - types where no finite value can be constructed due to unresolvable cyclic references.

Based on GraphQL spec proposal #1211, adapted for the validation-refactor branch architecture.

Problem

Self-referential or cyclical OneOf types without escape paths are uninhabited:

# Uninhabited - no way to construct a finite value
input A @oneOf { self: A }

# Also uninhabited - cycle with no escape
input A @oneOf { b: B }
input B @oneOf { a: A }

# Inhabited - list provides escape path
input A @oneOf { self: [A] }

# Inhabited - scalar provides escape path
input A @oneOf { b: B, escape: String }

Changes

  • SchemaValidationErrorType: Added OneOfNotInhabited error classification
  • TypeAndFieldRule: Inlined all OneOf validation logic into this consolidated schema validation rule class:
    • Added visitGraphQLInputObjectField method for field-level validation (nullable and no default value checks)
    • Added visitGraphQLInputObjectType method with recursive cycle detection via canBeProvidedAFiniteValue
    • Returns true if any field references: scalar, enum, list, or non-OneOf input type
    • Returns false if all fields form unresolvable OneOf cycles
  • SchemaValidator: Removed separate OneOfInputObjectRules from rules list (OneOf validation now part of TypeAndFieldRule)
  • Tests: 10 test cases covering inhabited cases (scalars, enums, lists, non-OneOf refs, escape paths) and uninhabited cases (self-reference, multi-type cycles)
Original prompt

Context

This PR re-implements the functionality from PR #4248 ("OneOf Inhabitability" by @jbellenger) but based on the validation-refactor branch (PR #4228), since #4228 will be merged first.

PR #4248 targets master and adds validation for uninhabited OneOf input types. Since PR #4228 significantly refactors validation (consolidating 31 rule classes into a single OperationValidator, removing AbstractRule, RulesVisitor, and individual rule classes), the OneOf inhabitability changes need to be adapted to work with the new validation architecture.

What needs to be implemented

The core feature is validating that OneOf input types are "inhabited" — i.e., that a finite value can be constructed for them. A self-referential OneOf like input A @oneOf { a: A } is uninhabited because you can never construct a finite value for it.

This is based on the spec proposal at graphql/graphql-spec#1211

Changes needed (adapted from PR #4248 to the validation-refactor architecture):

1. src/main/java/graphql/schema/validation/SchemaValidationErrorType.java
Add a new enum value OneOfNotInhabited (between OneOfNonNullableField and RequiredInputFieldCannotBeDeprecated).

2. src/main/java/graphql/schema/validation/OneOfInputObjectRules.java
Add a new method visitGraphQLInputObjectType that checks if a OneOf input object type is inhabited. If not, it adds a SchemaValidationError with type OneOfNotInhabited.

Add the following logic (from PR #4248):

@Override
public TraversalControl visitGraphQLInputObjectType(GraphQLInputObjectType inputObjectType, TraverserContext<GraphQLSchemaElement> 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<GraphQLInputObjectType> visited) {
    if (visited.contains(oneOfInputObject)) {
        return false;
    }
    Set<GraphQLInputObjectType> 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;
}

New imports needed:

import graphql.schema.GraphQLType;
import graphql.schema.GraphQLUnmodifiedType;
import java.util.LinkedHashSet;
import java.util.Set;

3. src/test/groovy/graphql/schema/validation/OneOfInputObjectRulesTest.groovy
Add the following 10 test cases after the existing tests:

  • "oneOf with scalar fields is inhabited" - input A @oneOf { a: String, b: Int } → no exception
  • "oneOf with enum field is inhabited" - input A @oneOf { a: Color } with enum Color { RED GREEN BLUE } → no exception
  • "oneOf with list field is inhabited" - input A @oneOf { a: [A] } → no exception
  • "oneOf referencing non-oneOf input is inhabited" - input A @oneOf { a: RegularInput } with input RegularInput { x: String } → no exception
  • "oneOf with escape field is inhabited" - input A @oneOf { b: B, escape: String } with input B @oneOf { a: A } → no exception
  • "mutually referencing oneOf types with scalar escape is inhabited" - input A @oneOf { b: B } with input B @oneOf { a: A, escape: Int } → no exception
  • "oneOf referencing non-oneOf with back-reference is inhabited" - input A @oneOf { b: RegularInput } with input RegularInput { back: A } → no exception
  • "multiple fields with chained oneOf escape is inhabited" - input A @oneOf { b: B, c: C }, input B @oneOf { a: A }, input C @oneOf { a: A, escape: String } → no exception
  • "single oneOf self-reference cycle is not inhabited" - input A @oneOf { self: A }InvalidSchemaException with 1 error of type OneOfNotInhabited
  • "multiple oneOf types forming cycle are not inhabited" - input A @oneOf { b: B }, `input B @oneo...

This pull request was created from Copilot chat.


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI changed the title [WIP] Re-implement OneOf inhabitability validation for new architecture Add OneOf Inhabitability validation Feb 19, 2026
Copilot AI requested a review from andimarek February 19, 2026 01:56
@andimarek
Copy link
Member

@copilot The rule should be inlined
Into the operationValidator class as every other rule

Copy link
Contributor Author

Copilot AI commented Feb 19, 2026

@copilot The rule should be inlined
Into the operationValidator class as every other rule

Done. Inlined all OneOf validation logic into TypeAndFieldRule class and removed the separate OneOfInputObjectRules file. Commit: 4890f7f

@andimarek andimarek marked this pull request as ready for review February 19, 2026 02:05
@andimarek
Copy link
Member

@copilot this PR should not start from master but from the PR we want to merge into

Copy link
Contributor Author

Copilot AI commented Feb 19, 2026

@copilot this PR should not start from master but from the PR we want to merge into

The PR is configured to target the validation-refactor branch (as indicated in the stacked PR context). The base commit "051f73d Remove unnecessary null checks on getCodeRegistry()" appears to be from that branch. If the base branch setting in GitHub shows master instead of validation-refactor, that would need to be changed in the PR settings on GitHub (which I cannot modify directly from this environment).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments