Skip to main content

OneOf Input Objects

At a glance​

Timeline​


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​ 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: πŸ‘€ πŸ˜‰β€‹