From 1e86f0aba3c746cdd5cdba47b8ea1bd7973c4067 Mon Sep 17 00:00:00 2001 From: James Bellenger Date: Mon, 16 Feb 2026 15:57:06 -0800 Subject: [PATCH 1/4] spec edits for OneOf inhabitability --- spec/Section 3 -- Type System.md | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 44760852c..9529e1602 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -1642,6 +1642,35 @@ input Second { } ``` +_OneOf Input Objects_ introduce another case where a field cannot be provided a +finite value. Because exactly one field must be set and must be non-null, +nullable fields do not provide an escape from recursion as they do in regular +Input Objects. This OneOf Input Object is invalid because its only field +references itself, and a non-null value must always be provided: + +```graphql counter-example +input Example @oneOf { + self: Example +} +``` + +However, a OneOf Input Object that references itself is valid when at least one +field provides a path to a type that is not a OneOf Input Object: + +```graphql example +input PetInput @oneOf { + cat: CatInput + dog: DogInput + self: PetInput +} +input CatInput { + name: String! +} +input DogInput { + name: String! +} +``` + **Result Coercion** An input object is never a valid result. Input Object types cannot be the return @@ -1730,6 +1759,8 @@ input ExampleInputObject { Input Objects, at least one of the fields in the chain of references must be either a nullable or a List type. 4. {InputObjectDefaultValueHasCycle(inputObject)} must be {false}. +5. If the Input Object is a _OneOf Input Object_, + {OneOfInputObjectCanBeProvidedAFiniteValue(inputObject)} must be {true}. InputObjectDefaultValueHasCycle(inputObject, defaultValue, visitedFields): @@ -1768,6 +1799,26 @@ InputFieldDefaultValueHasCycle(field, defaultValue, visitedFields): - Return {InputObjectDefaultValueHasCycle(namedFieldType, fieldDefaultValue, nextVisitedFields)}. +OneOfInputObjectCanBeProvidedAFiniteValue(oneOfInputObject, visited): + +- If {visited} is not provided, initialize it to the empty set. +- If {oneOfInputObject} is within {visited}: + - Return {false}. +- Let {nextVisited} be a new set containing {oneOfInputObject} and everything + from {visited}. +- For each field {field} of {oneOfInputObject}: + - Let {fieldType} be the type of {field}. + - If {fieldType} is a List type: + - Return {true}. + - Let {namedFieldType} be the underlying named type of {fieldType}. + - If {namedFieldType} is not an Input Object type: + - Return {true}. + - If {namedFieldType} is not a _OneOf Input Object_: + - Return {true}. + - If {OneOfInputObjectCanBeProvidedAFiniteValue(namedFieldType, nextVisited)}: + - Return {true}. +- Return {false}. + ### OneOf Input Objects :: A _OneOf Input Object_ is a special variant of _Input Object_ where exactly From 3d521c28547673f2504aaf60bc96477919564f48 Mon Sep 17 00:00:00 2001 From: James Bellenger Date: Thu, 26 Feb 2026 09:22:10 -0800 Subject: [PATCH 2/4] rework circular references section reframe the issue as as general class of input object habitation, not just a special edge case around pure OneOf trees. This allows handling the case of mixed OneOf/non-OneOf types, like: ```graphql input A @oneOf { b: B } input B { a: A! } ``` To support this, I've also rewritten the previously-proposed `OneOfInputObjectCanBeProvidedAFiniteValue` function as `InputObjectCanBeProvidedAFiniteValue`, which handles both pure inputs, pure OneOfs, and mixed input/OneOfs validation. --- spec/Section 3 -- Type System.md | 87 +++++++++++++++----------------- 1 file changed, 42 insertions(+), 45 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 9529e1602..3b1ab8229 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -1594,9 +1594,9 @@ Input Objects are allowed to reference other Input Objects as field types. A circular reference occurs when an Input Object references itself either directly or through referenced Input Objects. -Circular references are generally allowed, however they may not be defined as an -unbroken chain of Non-Null singular fields. Such Input Objects are invalid -because there is no way to provide a legal value for them. +Circular references are generally allowed, however they may not form cycles such +that no finite value can be provided. Such Input Objects are invalid because +there is no way to provide a legal value for them. This example of a circularly-referenced input type is valid as the field `self` may be omitted or the value {null}. @@ -1642,32 +1642,19 @@ input Second { } ``` -_OneOf Input Objects_ introduce another case where a field cannot be provided a -finite value. Because exactly one field must be set and must be non-null, -nullable fields do not provide an escape from recursion as they do in regular -Input Objects. This OneOf Input Object is invalid because its only field -references itself, and a non-null value must always be provided: +Because _OneOf Input Objects_ require exactly one field to be set and non-null, +nullable fields do not allow a finite value to be provided as they do in other +Input Objects. This example is invalid because providing a value for `First` +requires a non-null `Second`, and constructing a `Second` requires a non-null +`First`: ```graphql counter-example -input Example @oneOf { - self: Example +input First @oneOf { + second: Second } -``` - -However, a OneOf Input Object that references itself is valid when at least one -field provides a path to a type that is not a OneOf Input Object: -```graphql example -input PetInput @oneOf { - cat: CatInput - dog: DogInput - self: PetInput -} -input CatInput { - name: String! -} -input DogInput { - name: String! +input Second { + first: First! } ``` @@ -1755,12 +1742,8 @@ input ExampleInputObject { 5. If the Input Object is a _OneOf Input Object_ then: 1. The type of the input field must be nullable. 2. The input field must not have a default value. -3. If an Input Object references itself either directly or through referenced - Input Objects, at least one of the fields in the chain of references must be - either a nullable or a List type. +3. {InputObjectCanBeProvidedAFiniteValue(inputObject)} must be {true}. 4. {InputObjectDefaultValueHasCycle(inputObject)} must be {false}. -5. If the Input Object is a _OneOf Input Object_, - {OneOfInputObjectCanBeProvidedAFiniteValue(inputObject)} must be {true}. InputObjectDefaultValueHasCycle(inputObject, defaultValue, visitedFields): @@ -1799,25 +1782,39 @@ InputFieldDefaultValueHasCycle(field, defaultValue, visitedFields): - Return {InputObjectDefaultValueHasCycle(namedFieldType, fieldDefaultValue, nextVisitedFields)}. -OneOfInputObjectCanBeProvidedAFiniteValue(oneOfInputObject, visited): +InputObjectCanBeProvidedAFiniteValue(inputObject, visited): - If {visited} is not provided, initialize it to the empty set. -- If {oneOfInputObject} is within {visited}: +- If {inputObject} is in {visited}: - Return {false}. -- Let {nextVisited} be a new set containing {oneOfInputObject} and everything +- Let {nextVisited} be a new set containing {inputObject} and everything from {visited}. -- For each field {field} of {oneOfInputObject}: - - Let {fieldType} be the type of {field}. - - If {fieldType} is a List type: - - Return {true}. - - Let {namedFieldType} be the underlying named type of {fieldType}. - - If {namedFieldType} is not an Input Object type: - - Return {true}. - - If {namedFieldType} is not a _OneOf Input Object_: - - Return {true}. - - If {OneOfInputObjectCanBeProvidedAFiniteValue(namedFieldType, nextVisited)}: - - Return {true}. -- Return {false}. +- If {inputObject} is a _OneOf Input Object_: + - For each field {field} of {inputObject}: + - Let {fieldType} be the type of {field}. + - If {FieldTypeCanBeProvidedAFiniteValue(fieldType, nextVisited)}: + - Return {true}. + - Return {false}. +- Otherwise: + - For each field {field} of {inputObject}: + - Let {fieldType} be the type of {field}. + - If {fieldType} is Non-Null: + - Let {innerType} be the type wrapped by {fieldType}. + - If not {FieldTypeCanBeProvidedAFiniteValue(innerType, nextVisited)}: + - Return {false}. + - Return {true}. + +FieldTypeCanBeProvidedAFiniteValue(fieldType, visited): + +- If {fieldType} is a List type: + - Return {true}. +- If {fieldType} is a Non-Null type: + - Let {innerType} be the type wrapped by {fieldType}. + - Return {FieldTypeCanBeProvidedAFiniteValue(innerType, visited)}. +- Assert: {fieldType} is a named type. +- If {fieldType} is not an Input Object type: + - Return {true}. +- Return {InputObjectCanBeProvidedAFiniteValue(fieldType, visited)}. ### OneOf Input Objects From d73c3ea3cf1a044f51e5f28f8f9152dbc0563398 Mon Sep 17 00:00:00 2001 From: James Bellenger Date: Sat, 28 Feb 2026 04:28:23 -0800 Subject: [PATCH 3/4] feedback - rename InputObjectCanBeProvidedAFiniteValue to InputObjectHasUnbreakableCycle - rename FieldTypeCanBeProvidedAFiniteValue to InputFieldTypeHasUnbreakableCycle --- spec/Section 3 -- Type System.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 3b1ab8229..d9416cf0f 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -1742,7 +1742,7 @@ input ExampleInputObject { 5. If the Input Object is a _OneOf Input Object_ then: 1. The type of the input field must be nullable. 2. The input field must not have a default value. -3. {InputObjectCanBeProvidedAFiniteValue(inputObject)} must be {true}. +3. {InputObjectHasUnbreakableCycle(inputObject)} must be {false}. 4. {InputObjectDefaultValueHasCycle(inputObject)} must be {false}. InputObjectDefaultValueHasCycle(inputObject, defaultValue, visitedFields): @@ -1782,39 +1782,39 @@ InputFieldDefaultValueHasCycle(field, defaultValue, visitedFields): - Return {InputObjectDefaultValueHasCycle(namedFieldType, fieldDefaultValue, nextVisitedFields)}. -InputObjectCanBeProvidedAFiniteValue(inputObject, visited): +InputObjectHasUnbreakableCycle(inputObject, visited): - If {visited} is not provided, initialize it to the empty set. - If {inputObject} is in {visited}: - - Return {false}. + - Return {true}. - Let {nextVisited} be a new set containing {inputObject} and everything from {visited}. - If {inputObject} is a _OneOf Input Object_: - For each field {field} of {inputObject}: - Let {fieldType} be the type of {field}. - - If {FieldTypeCanBeProvidedAFiniteValue(fieldType, nextVisited)}: - - Return {true}. - - Return {false}. + - If not {InputFieldTypeHasUnbreakableCycle(fieldType, nextVisited)}: + - Return {false}. + - Return {true}. - Otherwise: - For each field {field} of {inputObject}: - Let {fieldType} be the type of {field}. - If {fieldType} is Non-Null: - - Let {innerType} be the type wrapped by {fieldType}. - - If not {FieldTypeCanBeProvidedAFiniteValue(innerType, nextVisited)}: - - Return {false}. - - Return {true}. + - Let {nullableType} be the unwrapped nullable type of {fieldType}. + - If {InputFieldTypeHasUnbreakableCycle(nullableType, nextVisited)}: + - Return {true}. + - Return {false}. -FieldTypeCanBeProvidedAFiniteValue(fieldType, visited): +InputFieldTypeHasUnbreakableCycle(fieldType, visited): - If {fieldType} is a List type: - - Return {true}. + - Return {false}. - If {fieldType} is a Non-Null type: - - Let {innerType} be the type wrapped by {fieldType}. - - Return {FieldTypeCanBeProvidedAFiniteValue(innerType, visited)}. + - Let {nullableType} be the unwrapped nullable type of {fieldType}. + - Return {InputFieldTypeHasUnbreakableCycle(nullableType, visited)}. - Assert: {fieldType} is a named type. - If {fieldType} is not an Input Object type: - - Return {true}. -- Return {InputObjectCanBeProvidedAFiniteValue(fieldType, visited)}. + - Return {false}. +- Return {InputObjectHasUnbreakableCycle(fieldType, visited)}. ### OneOf Input Objects From 3ef9ff7ab561ae75b8f44bb35f0aae61068ff426 Mon Sep 17 00:00:00 2001 From: James Bellenger Date: Tue, 3 Mar 2026 04:14:14 -0800 Subject: [PATCH 4/4] feedback apply Benjie's suggestion --- spec/Section 3 -- Type System.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index d9416cf0f..44ca849cc 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -1642,9 +1642,8 @@ input Second { } ``` -Because _OneOf Input Objects_ require exactly one field to be set and non-null, -nullable fields do not allow a finite value to be provided as they do in other -Input Objects. This example is invalid because providing a value for `First` +_OneOf Input Objects_ require exactly one field be provided, and that field +cannot be `null`. This example is invalid because providing a value for `First` requires a non-null `Second`, and constructing a `Second` requires a non-null `First`: