OneOf Input Objects
At a glanceβ
- Identifier: #825
- Stage: RFC2: Draft
- Champion: @benjie
- PR: OneOf Input Objects
- Related:
Timelineβ
- Added to 2024-07-18 WG agenda
- Commit pushed: Update spec/Section 3 -- Type System.md on 2024-06-05 by @benjie
- Commit pushed: Indicate `@oneOf` is a built-in directive on 2024-06-04 by @benjie
- 2 commits pushed on 2024-03-27:
- Commit pushed: Merge branch 'main' into oneof-v2 on 2023-11-13 by @benjie
- Added to 2022-12 WG agenda
- Mentioned in 2022-12 WG notes
- Commit pushed: Forbid 'extend input' from introducing the @oneOf directive on 2022-05-26 by @benjie
- 3 commits pushed on 2022-05-25:
- Commit pushed: Remove out of date example on 2022-05-06 by @benjie
- Added to 2022-05-05 WG agenda
- Mentioned in 2022-05-05 WG notes
- 4 commits pushed on 2022-03-22:
- Commit pushed: Update spec/Section 3 -- Type System.md on 2022-01-04 by @benjie
- Commit pushed: Apply suggestions from code review on 2021-12-23 by @benjie
- Added to 2021-10-07 WG agenda
- Mentioned in 2021-10-07 WG notes
- 2 commits pushed on 2021-04-08:
- 7 commits pushed on 2021-03-06:
- Much stricter validation for oneof literals (with examples) by @benjie
- Add missing coercion rule by @benjie
- Clearer wording of oneof coercion rule by @benjie
- Add more examples for clarity by @benjie
- Rename introspection fields to oneOf by @benjie
- Oneof's now require exactly one field/argument, and non-nullable vari⦠by @benjie
- Remove extraneous newline by @benjie
- Added to 2021-03-04 WG agenda
- Mentioned in 2021-03-04 WG notes
- Commit pushed: Fix typos (thanks @eapache!) on 2021-02-26 by @benjie
- Spec PR created on 2021-02-19 by benjie
- 3 commits pushed on 2021-02-19:
First came
the @oneField directive.Then there was
the Tagged type.Introducing: OneOf Input Objects
and OneOf Fields.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.oneField: Boolean
field, and in SDL via the@oneOf
directive on the input object.
OneOf Fields are a special variant of Object Type fields where the type system asserts that exactly one of the field's arguments must be set and non-null, all others being omitted. This is represented in introspection with the__Field.oneArgument: Boolean!
field, and in SDL via the@oneOf
directive on the field.(Why a directive? See the FAQ below.)
This variant introduces a form of input polymorphism to GraphQL. For example, the following
PetInput
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
}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?β
It's not. Well, not really - its an internal property of the type that's exposed through introspection - much in the same way that deprecation is. It just happens to be that after I analysed a number of potential syntaxes (including keywords and alternative syntax) I've found that the directive approach 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 π
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!
input FindUserBy @oneOf {
id: ID
organizationAndRegistrationNumber: OrganizationAndRegistrationNumberInput
}
input OrganizationAndRegistrationNumberInput {
organizationId: ID!
registrationNumber: Int!
}
type Query {
findUser(by: FindUserBy!): User
}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:
input FooBy @oneOf {
id: ID
str1: String
str2: String
}
type Query {
foo(by: FooBy!): 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β thatvalue
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: π πβ