Safer multi-option inputs with GraphQL @oneOf
Source: GraphQL blog
For most of GraphQL's history, "this argument is one of several mutually exclusive things" was a pattern you faked. Teams used input objects with several nullable fields plus a runtime check: at most one of url, upload, or externalId may be set; if zero or two arrive, throw. Easy to get wrong, hard for tools to reason about, and impossible for codegen to express as a sum type.
The September 2025 spec edition fixes this with `@oneOf` input objects.
OneOf input objects simplify schema entrypoints whilst maintaining type safety by expressing mutually exclusive inputs directly in the schema.
What `@oneOf` does
Apply @oneOf to an input object and the schema enforces a hard rule: clients must supply exactly one of its fields — never zero, never two:
input MediaSource @oneOf {
url: String
upload: Upload
externalId: ID
}
type Mutation {
createPost(media: MediaSource!): Post!
}Validation runs before the resolver, in the same pass that checks types and required fields. By the time your mutation fires, you've already eliminated the "what if both url and upload are set?" branch.
The pre-`@oneOf` patterns it replaces
Two anti-patterns dominated. Both still appear in older schemas, both have known costs:
- Multiple nullable fields with a runtime guard:
input MediaSource {
url: String
upload: Upload
externalId: ID
}The server checks Object.values(input).filter(Boolean).length === 1 and throws otherwise. Clients have no schema-level signal that the constraint exists; they discover it through documentation or by hitting a 400.
- Multiple mutations with overlapping behavior:
type Mutation {
createPostFromUrl(url: String!): Post!
createPostFromUpload(upload: Upload!): Post!
createPostFromExternalId(id: ID!): Post!
}Cleaner from a typing standpoint but multiplies the API surface and forces clients to know which mutation to reach for.
@oneOf collapses both patterns to a single mutation with a single, machine-checked input.
Codegen implications
This is where @oneOf pays off most. TypeScript codegen can now emit a discriminated union directly:
// Generated TypeScript
type MediaSource =
| { url: string; upload?: never; externalId?: never }
| { url?: never; upload: Upload; externalId?: never }
| { url?: never; upload?: never; externalId: string };The narrowing actually works: pick url, and TypeScript knows upload and externalId are unset; pass an object with two keys and TypeScript rejects at compile time. Equivalent guarantees exist for Swift (enum with associated values), Kotlin (sealed classes), and Rust (sum types).
Forms and CLIs
Anywhere a user picks one of several options — radio buttons, dropdowns, CLI subcommands — @oneOf collapses the schema-to-UI mapping:
- A radio group maps to the field names of the input object.
- A panel of inputs appears under the selected radio, populated from that field's type.
- Submission sends a single object with one populated key.
Validation, type narrowing, and rendering all flow from the same schema declaration. No hand-written guard.
AI and prompted clients
LLMs writing GraphQL queries previously had to learn the "set exactly one field" rule from documentation, comments, or examples. @oneOf changes that. The model reads the schema, sees the directive, and emits valid mutations on the first try. The error rate on mutations that previously needed prose explanations drops.
This connects to the broader theme of the September 2025 edition: AI-first clients benefit from schemas that encode constraints in the schema, not in the README.
Validation behavior
The spec defines the validation precisely:
- Zero fields supplied → query rejected.
- More than one field supplied → query rejected.
- Variables that resolve to ambiguous shapes → rejected at coercion time, before execution.
- Default values — input objects marked
@oneOfcannot have default values for their fields (would conflict with the "exactly one" rule).
Server implementations that conform to the September 2025 edition apply these rules automatically. graphql-js v17, Apollo Server, Yoga, and Mercurius all picked up support during late 2025 / early 2026.
Migration from existing schemas
If you have a "nullable input object with runtime guard" pattern, the migration is mechanical:
1. Add @oneOf to the input object declaration.
2. Remove the runtime guard from the resolver — the validator does it now.
3. Regenerate codegen.
4. Update clients that may have been sending multiple fields by accident.
For schemas with multiple sibling mutations, you have a choice: keep them for backwards compatibility (mark them deprecated, add the unified one), or migrate clients first and remove the duplicates. Either path is fine; the unified @oneOf mutation is the destination.
RFC trail and history
@oneOf landed as RFC #825 after years of discussion. The proposal went through multiple iterations on naming, validation rules, and interaction with other schema features (default values, variables, federation). The fact that it's in a named spec edition rather than a vendor extension is the important detail — every implementation that conforms to the September 2025 edition is required to support it.
Where this connects
Pair this post with the September 2025 edition announcement for the full context — @oneOf is one of several features that make schemas more legible to both humans and LLMs. The graphql-js v17 post covers the reference implementation; the Apollo Router 2.14 post covers the gateway side.
References
Source / further reading: GraphQL blog