More expressive schemas with @oneOf
Benoit Lubek
GraphQL, the lingua franca of APIs, offers a palette of expressive tools: interfaces, nullability, deprecation, unions, etc.
Occasionally though, a backend’s contract would be reflected more precisely, if only it could be represented in the schema. Take unions for example: they can be used for output types, but not input types, so a search API accepting different kinds of criteria can be implemented like this:
type Query {
searchByName(name: String!): User
searchById(id: ID!): User
searchBy...
}
This works, but is a bit tedious, and what we really mean is something like search(name: String! | id: ID! | …)
. What can we do to improve the situation?
GraphQL is an open standard, with a working group and processes in place to support its evolution: the RFCs (requests for comments).
The OneOf RFC
Seeing this possible room for improvement, the GraphQL community came up with this RFC, which proposes a new built-in directive: @oneOf
. It can be applied to input types to indicate that one and only one of the fields must be provided.
In our example above, we can take advantage of it like this:
type Query {
search(criteria: SearchCriteria!): User
}
input SearchCriteria @oneOf {
name: String
id: ID
#...
}
With that, a client can query search({ name: “John” })
or search({ id: “42” })
but search({ name: “John”, id: “42” })
would result in a validation error.
Backwards compatibility
A nice property of the proposal is that it doesn’t introduce any new syntax and therefore should be transparent for any client libraries that have no knowledge of the new directive.
If you provide more than one field to a OneOf input type, a “OneOf aware” library will warn you early, before executing the operation. An older library will treat the input type as a regular one, and will allow its execution, but the backend will then provide an error, as expected. To achieve this compatibility, all the fields of a OneOf input type must be declared as if they were nullable (without an exclamation mark).
input SearchCriteria @oneOf {
name: String
id: ID! # Illegal!
type: Type! # Illegal!
}
Without that requirement, pre-OneOf client libraries would require to provide all non-nullable fields – which wouldn’t make sense since only one field must be provided.
Note: although the fields are declared in the schema as nullable, passing the value null is not allowed. This may seem counterintuitive at first but the reason lies within GraphQL’s type system – this RFC comment explains it all.
Support in Apollo Kotlin
If you are an Apollo Kotlin user and want to try @oneOf today, you’re in luck! We’ve added experimental support for it in version 4:
- Fetching a schema via introspection on a OneOf-aware server will include the
@oneOf
directives.
- You’ll get a runtime error if you build a oneOf input type and provide more than one field:
SearchCriteria.Builder()
.name("Luke")
.id("42")
.build() // Throws! Must provide only one field.
- When using the IDE plugin you’ll get errors in the editor:
Support in Apollo Server
With Apollo Server, you can use @oneOf
in your schemas starting from version 4.
What’s next?
The GraphQL RFC process is an iterative one. The OneOf RFC is currently in Stage 2 (“Draft”), which means it is thought to be ready to be accepted into the specification but awaiting feedback from the community. Try it today with Apollo Kotlin and Apollo Server, and make your voice heard on the ticket and in the Apollo issue trackers.