Skip to main content

Input Objects accepting exactly @oneField

At a glance​

Timeline​


This is an RFC for adding explicit GraphQL support (and with it improved type safety) for a pattern that supports inputs of multiple types (composite and scalar).

Explanation of the problem​

Often a GraphQL field needs to accept one of many input types.

Example 1 (below) outlines the need for the addContent mutation to accept a range of different content types.

Example 2 (below) outlines the need for the findPosts record finder to accept a range of different filtering criteria.

Currently in GraphQL the type safe solution to this is to have different fields, one per type:

Example 1 (more details in "Example 1" section below):

  • addPostContent(post: [PostInput!]!)
  • addImageContent(image: [ImageInput!]!)
  • addHrefContent(href: [String!]!)

Note to add lots of content blocks of different types you must add multiple mutations to your GraphQL request, which is a lot less efficient and loses the transactional nature of the request.

Example 2 (more details in "Example 2" section below):

  • findPostsByIds(ids: [Int!])
  • findPostsMatchingConditions(conditions: [PostCondition!])

Another solution is the "tagged input union" pattern, which results in a less overwhelming schema thanks to fewer fields, solves the issue of inputting lots of data in a single mutation, but loses type safety:

input MediaBlock {
post: PostInput
image: ImageInput
href: String
}
extend type Mutation {
addContent(content: [MediaBlock!]!): AddContentPayload
}

(The loss of type safety is that each block should require​ that exactly one of the fields is set, however GraphQL does not enforce this so it must be done at run-time, and generic tooling will not be able to confirm the validity of inputs.)

Prior Art​

This RFC is similar to the "tagged input union" pattern discussed as an alternative in the inputUnion RFC #395 (please see the reasons people may not be happy with it there also). Since we are failing to reach consensus of on the inputUnion type in that proposal, I'd like to revisit Lee's original counter-proposal with the small addition of a directive to require​ that exactly one field be present on the input object (as originally proposed by Ivan).

The input object requiresExactlyOneField property (exposed via @oneField)​

This new requiresExactlyOneField introspection property for input objects can be exposed via IDL as the @oneField directive (in a similar way to how the deprecationReason introspection property is exposed as @deprecated(reason: ...)).

This property applies to an input object type:

input MyInput @oneField {
field1: Type1
field2: Type2
field3: Type3
}

(In the IDL, we could alternatively expose it as a different type: inputOne MyInput, rather than input MyInput @oneField, if using a directive was found to be confusing.)

The fields of an input object type defined with @oneField must all be nullable, and must not have default values. On input, exactly one field must be specified, and its value must not be null.

(For the avoidance of doubt: the field types may be scalars or input objects, and the same type may be specified for multiple fields. This is the same as for regular input objects.)

I've called the flag @oneField (and the related introspection field requireExactlyOneField), but it could equally be called @requireExactlyOneField, @exactlyOne, @one, or many other variants depending on your position in the terseness vs explicitness spectrum.

Example 1​

input PostInput {
title: String!
body: String!
}

input ImageInput {
photo: String!
caption: String
}

"""
Each media block can be one (and only one) of these types.
"""
input MediaBlock @oneField {
post: PostInput
image: ImageInput
href: String
}

type Mutation {
addContent(content: [MediaBlock!]!): Post
}
mutation AddContent($content: [MediaBlock!]!) {
addContent(content: $content) {
id
}
}

Example input:

{
content: [
{ post: { title: "@oneField directive", body: "..." } },
{ image: { photo: "https://..." } },
{ href: "https://graphql.org" }
]
}

Example 2​

"""
Options available for matching a post title
"""
input PostTitleFilter @oneField {
equals: String
contains: String
doesNotContain: String
}

"""
You may search within the title of the post, or within it's full text.
"""
input PostCondition @oneField {
title: PostTitleFilter
fullTextSearch: String
}

"""
Identify the relevant posts either by a list of IDs, or a list of conditions
for them to match.
"""
input PostMatch @oneField {
ids: [Int!]
conditions: [PostCondition!]
}

type Query {
findPosts(matching: PostMatch!): [Post!]
}
query FindPosts($matching: PostMatch!) {
findPosts(matching: $matching) {
id
title
}
}

Example inputs:

{
matching: {
ids: [27, 30, 93]
}
}
{
matching: {
conditions: [
{ fullTextSearch: "GraphQL" },
{ title: { contains: "Facebook" } }
]
}
}

Example 3​

input OrganizationAndEmailInput {
organization: String!
email: String!
}

"""
Conditions that uniquely identify a user, supply exactly one.
"""
input UserWhere @oneField {
id: ID
databaseId: Int
organizationAndEmail: OrganizationAndEmailInput
username: String
}

type Query {
getUser(where: UserWhere!): User
}
query GetUser($where: UserWhere!) {
getUser(where: $where) { id databaseId username email organization { id name slug } }
}

Example inputs:

{ where: { id: 27 } }
{ where: { username: "Benjie" } }
{
where: {
organizationAndEmail: {
organization: "graphql-wg",
email: "example@example.com"
}
}
}

Guiding principles​

Backwards Compatibility​

This change involves a small addition to the introspection system. Old clients will continue to work as they do currently, and can issue requests involving @oneField input objects without needing to be updated, they simply will not benefit from the type validation of knowing exactly one field is required.

All pre-existing queries (and schemas) will still be valid, and old clients can query servers that support this feature without loss of functionality.

Performance is a feature​

There is minimal overhead in this change; input objects continue to work as they did before, with one additional validation rule (that exactly one field must be specified). This can be asserted during the regular Input Object Required Fields Validation at very little cost. (In the spec I've written it up as a separate rule, but implementations can implement this more efficiently.)

Favour no change​

The change itself is small (and hopefully easy to implement), whilst bringing significant value to type safety. It's not possible to represent this use case in a type safe way in GraphQL currently, without having a proliferation of object type fields such as addPostMediaBlock, addImageMediaBlock, ... (which gets even more complex when there's nested concerns).

Enable new capabilities motivated by real use cases​

It's clear from #395 and #488 (plus the multiple discussions during GraphQL working groups, and the community engagement) that a feature such as this is required. It's currently possible to express this in GraphQL (simply omit the @oneField directive in the examples above), however it is not expressed in a type-safe way and requires the schema's resolvers to perform validation, resulting in run-time rather than compile-time errors (and no hinting in editors/tooling).

Simplicity and consistency over expressiveness and terseness​

I believe this has been achieved (bike-shedding over the directive name notwithstanding).

Preserve option value​

This RFC is only on using this directive with input objects, I have been deliberately strict to keep the conversation on topic. There is room for expansion (and even for the addition of an explicit inputUnion) in future.

Understandability is just as important as correctness​

I've done my best; please suggest edits that may make the spec changes more clear πŸ‘

Comparison with inputUnion​

The field name takes the place of the "discriminator" discussed in #395.

The implementation is significantly simpler, for example the only change required to support autocompletion in GraphiQL is to stop autocompleting once one field has been supplied.

The implementation is backwards compatible: older clients may query a schema that uses @oneField without loss of functionality.

The implementation is forwards compatible: newer clients may query a schema that implements an older GraphQL version without causing any issues (whereas the unexpected addition of __inputname may break existing GraphQL servers, hence complexity of only specifying it where a union is used).

There's no __inputname / __typename required, and heterogeneous input types are still supported.

The field types may be input objects or scalars.

This pattern is already in use in the wild, but with the execution layer handling validation, rather than the schema itself.

@oneField does not mirror output unions/interfaces.

@oneField increases the depth of input objects vs the inputUnion proposal (but, interestingly, does not increase the size of the request:

# `inputUnion` example
{ pets: [
{ __inputname: "Cat", lives: 3 },
{ __inputname: "Dog", wagging: true }
]}
# `@exactlyOne` example
{ pets: [
{ cat: { lives: 3 } }
{ dog: { wagging: true } }
]}

Potential concerns, challenges and drawbacks​

  • It's not called an input union, and does not mirror the union syntax for output types
  • It is not an ideal syntax (is there one?), and adds an additional layer of depth to input objects
  • It requires a small addition to the introspection system: requiresExactlyOneField

This pattern is already used in the wild, and the introduction of this feature will not break any existing APIs. APIs that already use this pattern could benefit from this feature retroactively without needing to update any any existing clients.