Skip to content

OneOf Inhabitability#4564

Merged
yaacovCR merged 17 commits into
graphql:17.x.xfrom
jbellenger:jbellenger-oneof-inhabitability
Jun 11, 2026
Merged

OneOf Inhabitability#4564
yaacovCR merged 17 commits into
graphql:17.x.xfrom
jbellenger:jbellenger-oneof-inhabitability

Conversation

@jbellenger

@jbellenger jbellenger commented Feb 17, 2026

Copy link
Copy Markdown
Contributor

This PR implements the validation described at graphql/graphql-spec#1211

More discussion is at the link above, but the core issue is around uninhabited OneOf types -- the property that a type can be defined in a way that a value cannot be generated for it.

The simplest example of an uninhabited type is this recursive OneOf:

input A @oneOf { a:A }

While this type definition is currently valid by the rules of OneOf types and circular input types, it violates the spirit of the Circular References because it does not allow construction of a finite value.

This PR updates the existing cycle validation to consider OneOf types. I've tried to follow the existing conventions, though I'm also new here and might have missed something. Any feedback on style or substance is welcome.

@vercel

vercel Bot commented Feb 17, 2026

Copy link
Copy Markdown

@jbellenger is attempting to deploy a commit to the The GraphQL Foundation Team on Vercel.

A member of the Team first needs to authorize it.

@jbellenger jbellenger changed the base branch from 16.x.x to v17-alpha February 17, 2026 12:29
@jbellenger jbellenger changed the base branch from v17-alpha to next February 17, 2026 12:35
@jbellenger jbellenger marked this pull request as ready for review February 17, 2026 13:29
@jbellenger jbellenger requested a review from a team as a code owner February 17, 2026 13:29
@jbellenger jbellenger force-pushed the jbellenger-oneof-inhabitability branch from 64e8899 to bd3e1bf Compare February 26, 2026 20:27

@benjie benjie left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great tests! (It's 10:30pm so I've not tried to review the code... 😅)

@jbellenger jbellenger force-pushed the jbellenger-oneof-inhabitability branch from bd3e1bf to ba19546 Compare February 28, 2026 12:51
@benjie benjie added the spec RFC Implementation of a proposed change to the GraphQL specification label Mar 5, 2026
Comment thread src/type/validate.ts Outdated
@yaacovCR yaacovCR force-pushed the jbellenger-oneof-inhabitability branch from ba19546 to c5a600f Compare May 6, 2026 03:06
@yaacovCR yaacovCR changed the base branch from next to 17.x.x May 6, 2026 03:06
@yaacovCR yaacovCR closed this May 6, 2026
@yaacovCR yaacovCR reopened this May 6, 2026
jbellenger and others added 7 commits June 10, 2026 14:46
Rework the circular references validation to detect input object types
that cannot be provided a finite value. This covers:

- Self-recursive OneOf types (e.g. input A @OneOf { a: A })
- Mixed OneOf/non-OneOf cycles with no escape path
- Standard non-null circular references (existing behavior preserved)

Rename algorithms to match spec terminology:
- InputObjectHasUnbreakableCycle (was InputObjectCanBeProvidedAFiniteValue)
- InputFieldTypeHasUnbreakableCycle (was FieldTypeCanBeProvidedAFiniteValue)
- add memoization to improve validation performance
- graceful handling of empty oneof defintions
- add missing test coverage
Update error reporting for uninhabited cycles to match the previous
implementation, which would report 1 error per distinct cycle.

Also, update the errors to use the spec language "cannot be provided a
finite value"
@yaacovCR yaacovCR force-pushed the jbellenger-oneof-inhabitability branch from c4706c3 to fa0f160 Compare June 10, 2026 16:34
@yaacovCR

yaacovCR commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

@jbellenger

I finally think I understand everything here and this is awesome.

I pushed some changes/reorg added another memoization layer.

@yaacovCR yaacovCR force-pushed the jbellenger-oneof-inhabitability branch from 6ef16f3 to 2def7ac Compare June 10, 2026 18:19
@yaacovCR

Copy link
Copy Markdown
Contributor

@jbellenger

to add some detail

image
  • meaning adding another layer of memoization in b9acf03 grabbed back some of the perf drop
  • using that same map when reporting in 9ed3ad6 / HEAD doesn't add much perf wise, although it perhaps cleans the code a bit (?)

it's all somewhat slower than baseline 17.x.x but of course it's doing more (and correct) validation.

@yaacovCR yaacovCR force-pushed the jbellenger-oneof-inhabitability branch from 2def7ac to ad79f2a Compare June 11, 2026 15:28
yaacovCR added 2 commits June 11, 2026 18:40
The previous InputObjectHasUnbreakableCycle check ran once for every Input Object. Even with memoization, a type whose result was still unknown had to collect the Input Objects reachable from that root before it could prove which nodes were breakable. Shared OneOf and non-null Input Object subgraphs could therefore be walked from multiple roots before the memo settled.

Instead, initialize one state for every Input Object while validating the schema. detectInputObjectUnbreakableCycles then performs a single shallow pass over those states, recording only the edges that can force an unbreakable cycle: OneOf fields whose type is another Input Object, and non-OneOf fields whose type is a non-null Input Object. Fields with scalar, enum, list, or nullable escapes never become targets. Types with an immediate escape are pushed onto a worklist, and the worklist propagates that breakability through reverse dependents until only states with unbreakable cycles remain.

Once that fixed point is reached, detectInputObjectUnbreakableCycles reports cycles directly from the remaining states. Reporting cannot safely happen during the initial edge-discovery pass because a state that first appears cyclic may later be proven breakable by worklist propagation from one of its targets.

For example, consider a chain like T16 @OneOf { a: T15, b: T15 }, ... T1 @OneOf { a: T0, b: T0 }, T0 @OneOf { self: T0 }. The old per-root check can rediscover the same T15..T0 tail while checking different roots. The global pass calls getFields once per Input Object, records each Tn -> Tn-1 dependency once, and then propagates results through the dependent lists. That avoids repeated reachable-subgraph collection on schemas with shared input-object tails, which is the validateSchema regression this fixes.
@yaacovCR yaacovCR force-pushed the jbellenger-oneof-inhabitability branch from ad79f2a to ac64e04 Compare June 11, 2026 16:10
@yaacovCR

Copy link
Copy Markdown
Contributor

ok, we now have parity:

image

we avoid a full reachability calculation, but just iterate through all input objects once saving metadata, and then backwards propagate

@yaacovCR

yaacovCR commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

@jbellenger

Do we want (A) an error per input object cycles, or do we want (B) one error per input object listing all the cycles it has? Let's assume that both options filter so that we never report the same cycle more than once, but see the change in the last commit, we do (B), which would apply to non-OneOf cycles as well? In v16 (and prior to that commit ) we did (A), and have for a while with regard to non-OneOf cycles, but I think whatever we do should be consistent for both.

I feel like @benjie weighed in on this at the graphql-wg discussion about it?

@jbellenger

Copy link
Copy Markdown
Contributor Author

@yaacovCR Thanks for the polish you've put on this!

Re reporting, I think the wg discussion was here: https://youtu.be/0JP4x-Uf2qw?si=qhrpHaOaKFxYKsoO&t=997

My takeaway was that we'd want one error per cause, which I think means option B. This would also have the nice property of providing complete diagnostic information to an agent.

@yaacovCR

Copy link
Copy Markdown
Contributor

Shoot, I edited my comment. I think A and B were technically the same but I think it's right now.

@yaacovCR

Copy link
Copy Markdown
Contributor

Hmmm I guess the preference I read was toward A ie keep doing what we were doing, we should get a different error per cycle as the cycle was the cause not the input object per se…. I’m going to roll back that last commit…..

to demonstrate that OneOf behavior is parallel to non-OneOf
@yaacovCR yaacovCR force-pushed the jbellenger-oneof-inhabitability branch from ac64e04 to b967e36 Compare June 11, 2026 18:30
@yaacovCR

Copy link
Copy Markdown
Contributor

Basically, we can table that debate for this PR as it also changes how errors have been reported with regard to circular Input Objects for quite some time. If we want to change how multiple cycles starting at the same Input Object works, we should do so in lockstep across non-OneOf and OneOf cycles, and do so in a separate PR. I am pretty much agnostic, leaning toward grouping together; at the bottom line, we have all of the information in both variations both with regards to the messages and erroring location, the question is only the grouping.

@jbellenger

Copy link
Copy Markdown
Contributor Author

@yaacovCR

If we want to change how multiple cycles starting at the same Input Object works, we should do so in lockstep across non-OneOf and OneOf cycles, and do so in a separate PR.

Big plus one.

@yaacovCR yaacovCR merged commit 0e1e368 into graphql:17.x.x Jun 11, 2026
22 of 23 checks passed
@yaacovCR yaacovCR added the PR: bug fix 🐞 requires increase of "patch" version number label Jun 15, 2026
yaacovCR added a commit that referenced this pull request Jun 15, 2026
## v17.0.0 (2026-06-15)

#### New Feature 🚀
* [#4819](#4819) feat: graduate directives on directives ([@yaacovCR](https://github.com/yaacovCR))

#### Bug Fix 🐞
* [#4799](#4799) fix: raise request error on invalid fragment variables ([@yaacovCR](https://github.com/yaacovCR))
* [#4800](#4800) fix: apply directives when SDL contains type definitions and extensions with directives ([@yaacovCR](https://github.com/yaacovCR))
* [#4564](#4564) OneOf Inhabitability ([@jbellenger](https://github.com/jbellenger))
* [#4814](#4814) fix(KnownDirectivesRule): locations for input field arguments in extensions ([@yaacovCR](https://github.com/yaacovCR))
* [#4726](#4726) feat(validation): reject directive definition cycles ([@yaacovCR](https://github.com/yaacovCR))
* [#4815](#4815) Revert "feat(validation): reject directive definition cycles (#4726)" ([@yaacovCR](https://github.com/yaacovCR))

#### Docs 📝
<details>
<summary> 11 PRs were merged </summary>

* [#4790](#4790) docs: minor diagnostics doc comment improvements ([@yaacovCR](https://github.com/yaacovCR))
* [#4791](#4791) fix: docs: polish diagnostics comments further ([@yaacovCR](https://github.com/yaacovCR))
* [#4793](#4793) docs: further improve general execution and tracing docs ([@yaacovCR](https://github.com/yaacovCR))
* [#4802](#4802) docs: correct extension field comments - v17 ([@yaacovCR](https://github.com/yaacovCR))
* [#4805](#4805) docs: publish fixed extensions comments ([@yaacovCR](https://github.com/yaacovCR))
* [#4807](#4807) docs: add prettier for jsdoc examples ([@yaacovCR](https://github.com/yaacovCR))
* [#4435](#4435) Subscriptions docs suggestions ([@Urigo](https://github.com/Urigo))
* [#4811](#4811) docs: fix a few indentations inside string literals ([@yaacovCR](https://github.com/yaacovCR))
* [#4813](#4813) internal: docs update ([@yaacovCR](https://github.com/yaacovCR))
* [#4820](#4820) docs: document `@experimental_disableErrorPropagation` ([@yaacovCR](https://github.com/yaacovCR))
* [#4817](#4817) docs: post 17.rc-0 update ([@yaacovCR](https://github.com/yaacovCR))
</details>

#### Polish 💅
<details>
<summary> 2 PRs were merged </summary>

* [#4809](#4809) internal: use prettier for non-generated website files ([@yaacovCR](https://github.com/yaacovCR))
* [#4812](#4812) polish: fix stream test cases ([@yaacovCR](https://github.com/yaacovCR))
</details>

#### Internal 🏠
<details>
<summary> 6 PRs were merged </summary>

* [#4795](#4795) chore: move website publishing from 16.x.x to 17.x.x ([@yaacovCR](https://github.com/yaacovCR))
* [#4796](#4796) internal: fix broken npm/deno deployments ([@yaacovCR](https://github.com/yaacovCR))
* [#4797](#4797) ci: update GitHub Actions versions ([@yaacovCR](https://github.com/yaacovCR))
* [#4806](#4806) internal: update frontmatter ([@yaacovCR](https://github.com/yaacovCR))
* [#4808](#4808) intenral: fix ci badge ([@yaacovCR](https://github.com/yaacovCR))
* [#4810](#4810) internal: add prettier:examples to lint-staged ([@yaacovCR](https://github.com/yaacovCR))
</details>

#### Committers: 3
* James Bellenger([@jbellenger](https://github.com/jbellenger))
* Uri Goldshtein([@Urigo](https://github.com/Urigo))
* Yaacov Rydzinski ([@yaacovCR](https://github.com/yaacovCR))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

PR: bug fix 🐞 requires increase of "patch" version number spec RFC Implementation of a proposed change to the GraphQL specification

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants