Input Objects accepting exactly @oneField
At a glanceβ
- Identifier: #586
- Stage: RFCX: Closed 2019-11-05T22:43:17Z
- Champion: @benjie
- PR: Input Objects accepting exactly @oneField
- Related:
- #395 (inputUnion type)
- #733 (Tagged type)
- #825 (OneOf Input Objects)
- InputUnion (GraphQL Input Union)
Timelineβ
- 2 commits pushed on 2019-07-22:
- Commit pushed: Tweak wording for when IDL directive is required on 2019-06-13 by @benjie
- Commit pushed: Tweak wording: accepts -> requires on 2019-06-12 by @benjie
- Mentioned in 2019-06-06 WG notes
- Commit pushed: may -> must on 2019-05-23 by @benjie
- Spec PR created on 2019-05-18 by benjie
- Commit pushed: Add specification changes for @oneField directive on 2019-05-18 by @benjie
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 theinputUnion
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 thedeprecationReason
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 thaninput 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 fieldrequireExactlyOneField
), 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 theinputUnion
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 theunion
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.