At a glance
- Identifier: #825
- Stage: RFC 3 / Accepted
- Champion: @benjie
- Latest activity: 5 commits pushed on 2025-09-01
- Spec PR: https://github.com/graphql/graphql-spec/pull/825
- Related:
Spec PR description
Follow up of the @oneField directive and the Tagged type.
Introducing: OneOf Input Objects.
OneOf Input Objects are a special variant of Input Objects where the type system asserts that exactly one of the fields must be set and non-null, all others being omitted. This is represented in introspection with the __Type.isOneOf: Boolean field, and in SDL via the @oneOf directive on the input object.
This variant of an input object introduces a form of input polymorphism to GraphQL.
Example 1 - addPet
The following PetInput oneof input object lets you choose between a number of potential input types:
input PetInput @oneOf {
cat: CatInput
dog: DogInput
fish: FishInput
}
input CatInput { name: String!, numberOfLives: Int }
input DogInput { name: String!, wagsTail: Boolean }
input FishInput { name: String!, bodyLengthInMm: Int }
type Mutation {
addPet(pet: PetInput!): Pet
}
Example 2 - user(by:)
Previously you may have had a situation where you had multiple ways to locate a user:
type Query {
user(id: ID!): User
userByEmail(email: String!): User
userByUsername(username: String!): User
userByRegistrationNumber(registrationNumber: Int!): User
}
with OneOf Input Objects you can now express this via a single field without loss of type safety:
input UserBy @oneOf {
id: ID
email: String
username: String
registrationNumber: Int
}
type Query {
user(by: UserBy!): User
}
FAQ
Why is this a directive?
At its core, it's a property of the type that's exposed through introspection - much in the same way that deprecation is. There's nothing in introspection, nor in the types exposed through the reference implementation (new GraphQLInputObjectType({ name: "...", isOneOf: true, ... })) that relates to directives. It just happens to be that after I analysed a number of potential syntaxes (including keywords and alternative syntax) I've found that when representing the schema as SDL, using a directive to do so is the least invasive (all current GraphQL parsers can already parse it!) and none of the alternative syntaxes sufficiently justified the increased complexity they would introduce.
Why is this a good approach?
This approach, as a small change to existing types, is the easiest to adopt of any of the solutions we came up with to the InputUnion problem. It's also more powerful in that it allows additional types to be part of the "input union" - in fact any valid input type is allowed: input objects, scalars, enums, and lists of the same. Further it can be used on top of existing GraphQL tooling, so it can be adopted much sooner. Finally it's very explicit, so doesn't suffer the issues that "duck typed" input unions could face.
Why did you go full circle via the tagged type?
When the @oneField directive was proposed some members of the community felt that augmenting the behaviour of existing types might not be the best approach, so the Tagged type was born. (We also researched a lot of other approaches too.) However, the Tagged type brought with it a lot of complexity and controversy, and the Input Unions Working Group decided that we should revisit the simpler approach again. This time around I'm a lot better versed in writing spec edits :grin:
Why are all the fields nullable? Shouldn't they be non-nullable?
To make this change minimally invasive I wanted:
- to make it so that existing GraphQL clients could still validate queries against a oneOf-enabled GraphQL schema (if the fields were non-nullable the clients would think the query was invalid because it didn't supply enough data)
- to allow existing GraphQL implementations to change as little code as possible
To accomplish this, we add the "exactly one value, and that value is non-null" as a validation rule that runs after all the existing validation rules - it's an additive change.
Can this allow a field to accept both a scalar and an object?
Yes!
type Query {
findUser(by: FindUserBy!): User
}
input FindUserBy @oneOf {
id: ID
organizationAndRegistrationNumber: OrganizationAndRegistrationNumberInput
}
input OrganizationAndRegistrationNumberInput {
organizationId: ID!
registrationNumber: Int!
}
Can I use existing GraphQL clients to issue requests to OneOf-enabled schemas?
Yes - so long as you stick to the rules of one field / one argument manually - note that GraphQL already differentiates between a field not being supplied and a field being supplied with the value null.
Without explicit client support you may lose a little type safety, but all major GraphQL clients can already speak this language. Given this nonsense schema:
type Query {
foo(by: FooBy!): String
}
input FooBy @oneOf {
id: ID
str1: String
str2: String
}
the following are valid queries that you could issue from existing GraphQL clients:
{foo(by:{id: "..."})}{foo(by:{str1: "..."})}{foo(by:{str2: "..."})}query Foo($by: FooBy!) {foo(by: $by)}
If my input object has only one field, should I use @oneOf?
Doing so would preserve your option value - making a OneOf Input Object into a regular Input Object is a non-breaking change (the reverse is a breaking change). In the case of having one field on your type changing it from oneOf (and nullable) to regular and non-null is a non-breaking change (the reverse is also true in this degenerate case). The two Example types below are effectively equivalent - both require that value is supplied with a non-null int:
input Example @oneOf {
value: Int
}
input Example {
value: Int!
}
Can we expand @oneOf to output types to allow for unions of objects, interfaces, scalars, enums and lists; potentially replacing the union type?
:shushing_face: :eyes: :wink:
Timeline
- 5 commits pushed on 2025-09-01:
- leebyron committed "copy tweaks and remove redundant examples"
- leebyron committed "dedicated subsection"
- leebyron committed "rogue plural + links"
- leebyron committed "sp"
- leebyron committed "sp"
- 8 commits pushed on 2025-07-04:
- benjie committed "Merge branch 'main' into oneof-v2"
- benjie committed "execution error -> request error (input coercion)"
- benjie committed "Simplify and clarify OneOf Input Object additional coercion rules"
- benjie committed "Clarity and correctness"
- benjie committed "Simplify"
- benjie committed "Use a colon"
- benjie committed "Use the correct error for the situation"
- benjie committed "Remove example which will not always fail until #1059 is adopted"
- 2 commits pushed on 2025-07-01:
- leebyron committed "editorial: define and link _OneOf Input Object_"
- leebyron committed "Merge branch 'main' into oneof-v2"
- 4 commits pushed on 2025-06-12:
- benjie committed "Update spec/Section 3 -- Type System.md"
- benjie committed "Whitespace"
- benjie committed "Clarify algorithm"
- benjie committed "Rename 'Tagged'"
- Added to WG agenda on 2025-06-05
- 2 commits pushed on 2025-05-22:
- benjie committed "Merge branch 'main' into oneof-v2"
- benjie committed "Use 'execution error' and 'raise' rather than throw an error"
- Added to WG agenda on 2025-05-01
- Added to WG agenda on 2025-04-03
- Commit pushed on 2025-03-07 by benjie: Merge branch 'main' into oneof-v2
- 5 commits pushed on 2024-10-17:
- benjie committed "Clarify IsNonNullPosition algorithm"
- benjie committed "Clarify OneOf examples"
- benjie committed "Add more examples"
- benjie committed "Remove new validation rule in favour of updates to existing rules"
- benjie committed "Null literal is separate"
- Added to WG agenda on 2024-10-03
- Mentioned in WG notes on 2024-10-01
- Commit pushed on 2024-09-21 by yaacovCR: remove OneOf-specific rule in favor of update to VariablesInAllowedPo…
- Commit pushed on 2024-07-19 by benjie: Merge branch 'main' into oneof-v2
- Added to WG agenda on 2024-07-18
- Mentioned in WG notes on 2024-07-01
- Commit pushed on 2024-06-05 by benjie: Update spec/Section 3 -- Type System.md
- Commit pushed on 2024-06-04 by benjie: Indicate
@oneOfis a built-in directive - 2 commits pushed on 2024-03-27:
- benjie committed "Merge branch 'main' into oneof-v2"
- benjie committed "Add yet more examples to the example coercion table"
- Commit pushed on 2023-11-13 by benjie: Merge branch 'main' into oneof-v2
- Added to WG agenda on 2022-12-01
- Mentioned in WG notes on 2022-12-01
- Commit pushed on 2022-05-26 by benjie: Forbid 'extend input' from introducing the @oneOf directive
- 3 commits pushed on 2022-05-25:
- benjie committed "Rename __Type.oneOf to __Type.isOneOf"
- benjie committed "Add a:null example"
- benjie committed "Rewrite to avoid ambiguity of language"
- Commit pushed on 2022-05-06 by benjie: Remove out of date example
- Added to WG agenda on 2022-05-05
- Mentioned in WG notes on 2022-05-05
- 4 commits pushed on 2022-03-22:
- benjie committed "Merge branch 'main' into oneof-v2"
- benjie committed "Remove Oneof Fields from spec"
- benjie committed "Oneof -> OneOf"
- benjie committed "Spellings"
- Commit pushed on 2022-01-04 by benjie: Update spec/Section 3 -- Type System.md
- Commit pushed on 2021-12-23 by benjie: Apply suggestions from code review
- Added to WG agenda on 2021-10-07
- Mentioned in WG notes on 2021-10-07
- 2 commits pushed on 2021-04-08:
- benjie committed "graphgl -> graphql"
- benjie committed "Apply suggestions from @eapache's review"
- 7 commits pushed on 2021-03-06:
- benjie committed "Much stricter validation for oneof literals (with examples)"
- benjie committed "Add missing coercion rule"
- benjie committed "Clearer wording of oneof coercion rule"
- benjie committed "Add more examples for clarity"
- benjie committed "Rename introspection fields to oneOf"
- benjie committed "Oneof's now require exactly one field/argument, and non-nullable vari…"
- benjie committed "Remove extraneous newline"
- Added to WG agenda on 2021-03-04
- Mentioned in WG notes on 2021-03-04
- Commit pushed on 2021-02-26 by benjie: Fix typos (thanks @eapache!)
- Spec PR created on 2021-02-19 by benjie
- 3 commits pushed on 2021-02-19:
- benjie committed "Renumber list items"
- benjie committed "@oneOf input objects"
- benjie committed "@oneOf fields"