How do you design a GraphQL input where exactly one of several alternatives must be supplied?
answer
- The input side has no sum type
- Three nullable fields and a promise
- A constraint tooling cannot see
- Separate fields move it into validation
- A draft directive, not a released edition
basics
~20 sReleased GraphQL editions cannot express "exactly one of", so the usual shape is several nullable fields plus a runtime check. The alternatives are one schema field per case, or the draft @oneOf input marking where servers support it.
solid answer
~50 sThree shapes. The default is an input object whose alternatives are all nullable — `menuItemId`, `sku`, `custom` on a restaurant ordering graph's `AddItemInput` — with the server rejecting zero or two of them at runtime. It works, but the constraint is invisible: document validation, introspection and a typed client generator all see three independently optional fields, so callers discover the rule by failing a request. The second shape moves the choice into the schema as one field or mutation per alternative, which validation and a typed client generator can enforce, at the cost of duplicated arguments and a wider surface. The third is `@oneOf`, a **draft** type-system directive marking an input object as exactly-one; where a server implements it, the server enforces the rule and every member must be nullable with no default. It is not in a released edition, so treat adopting it as a portability decision rather than a default.
code
graphql · 7 linesinput AddItemInput {
orderId: ID!
quantity: Int! = 1
menuItemId: ID
sku: String
custom: CustomItemInput
}go deeper
Know the shape you will actually meet: several nullable fields where only one may be filled in, with the rule stated in the field's description rather than anywhere the type system can check it.
Explain why validation cannot catch a caller that supplies two of them, where the check therefore has to live, and what the generated client types look like as a result.
Weigh the three shapes out loud — nullable siblings, one field per alternative, the draft marking — and say which you would ship, what it costs consumers, and how a caller finds out they got it wrong.
Own the portability call. Adopting a draft directive commits every consumer's server and tooling to understanding it; decide whether the schema buys enough by it, and settle on one house pattern instead of three.
## Why the schema cannot say it Some inputs are genuinely a choice. On a restaurant ordering graph, adding a line to an order can identify the item three ways: by its catalogue id, by a supplier `sku`, or as a one-off custom item with its own name and price. Exactly one of those must be supplied, and supplying two is as wrong as supplying none. The released editions of the GraphQL specification have no way to express that. An input object is a **product** — every field is independent — and there is no sum type on the input side; a union is an output-only construct and cannot be used where an input type is required. So the constraint has to live somewhere other than the type. ## Shape 1: nullable siblings plus a runtime check The default shape, and the one you will meet in most real schemas: ```graphql input AddItemInput { orderId: ID! quantity: Int! = 1 menuItemId: ID sku: String custom: CustomItemInput } ``` All three alternatives are nullable, the rule is written in the description, and the server counts how many were provided and rejects zero or two. It works. What it costs is **visibility**: * Document validation sees three independently optional fields and passes a request that sets all three. * Introspection publishes no constraint, so an explorer, a schema-linting rule and a typed client generator all describe the input as three optional members. * Every consumer learns the rule by having a request rejected, which is the class of error a typed schema exists to remove. * The check is hand-written per input. A 4-person platform team with nine such inputs has nine slightly different checks and nine slightly different messages. ## Shape 2: one field per alternative Move the choice up into the schema, where validation already enforces required arguments: ```graphql type Mutation { addCatalogueItem(orderId: ID!, menuItemId: ID!, quantity: Int! = 1): AddItemPayload addCustomItem(orderId: ID!, custom: CustomItemInput!, quantity: Int! = 1): AddItemPayload } ``` Now "exactly one" is structural: you called one field or the other, and each one's required arguments are Non-Null, so a caller who omits them fails validation before execution and a generated client will not compile with the wrong combination. This is the right answer more often than people expect, and it is *clearly* right when the alternatives are not interchangeable — when picking the custom branch also requires a price and a tax category that the other branches must not carry. Squeezing those into one input object produces fields whose validity depends on a sibling, which is the same problem one level down. The cost is surface area: shared arguments get duplicated, and with four alternatives and two dimensions the field count grows faster than anyone wants to document. ## Shape 3: the draft @oneOf marking `@oneOf` is a **draft** type-system directive that marks an input object as exactly-one. Where a server implements it, the server enforces the rule during coercion rather than leaving it to your code, and the marking is exposed through introspection so tooling can see it too: ```graphql input AddItemSelectorInput @oneOf { menuItemId: ID sku: String custom: CustomItemInput } ``` Its rules are strict by design: every member must be nullable and carry no default, and a request must provide exactly one member with a non-null value. That strictness is what makes it checkable. The caution is that it is **not part of a released edition**. Support is uneven across servers, and anything sitting between your clients and your service — a router, a proxy that validates, a code generator, an explorer — has to understand it too, or it degrades into shape 1 with extra steps. Adopting it is a portability decision made once for the whole schema, not a per-input convenience. ## Evolution Adding an alternative is safe: existing senders keep sending what they always sent, and the new member is just one more thing they may ignore. **Removing** one is not. A shipped mobile build pinned to the removed field has its whole document rejected at validation, and unlike a server you cannot redeploy it — you can only wait for adoption. Retire an alternative on the client's release cadence: deprecate it, watch field usage until it reaches zero, then delete. Converting an existing plain input to the draft marking is also breaking in two directions at once: any caller sending two members starts failing, and any member that was Non-Null or defaulted has to be relaxed first. ## How to decide Ask three questions. Do the alternatives share the rest of their arguments, or does each branch drag its own? If each drags its own, split the field. Does the choice change what the operation *does*, or only where the same operation looks the item up? A pure lookup difference belongs in one input. And can every consumer's server and tooling handle a draft directive? If not, ship nullable siblings, put the rule in the description in the exact words the error uses, and enforce it in one shared helper rather than nine.
- What does the all-nullable shape cost a typed client generator's output?It emits three independently optional members, so caller code compiles while sending none of them or all three. The constraint survives only in a description string and in the server's runtime check, which means every consumer discovers it by failing a request rather than by failing a build — exactly the class of error a typed schema is supposed to remove.
- When is one schema field per alternative better than a single exactly-one input?When the alternatives do not share the rest of their arguments, or lead to different behaviour. If choosing the custom branch also requires a price and a tax category that the other branches must not carry, one input object collects fields whose validity depends on a sibling. Two fields, each with its own Non-Null arguments, let validation reject the wrong combination before execution.
- Is adding a fourth alternative to such an input a breaking change?Adding one is safe — existing senders keep sending what they always sent. Removing one is not: a shipped mobile build pinned to the removed field has its whole document rejected at validation, and you cannot redeploy it the way you redeploy a server. Deprecate, watch field usage until it reaches zero, then delete on the client's release cadence.
"Choose one" printed on a menu is a note to the diner; the waiter refusing an order with two mains is the enforcement. Nullable siblings give you the note without the waiter.
saying these in an interview costs you the question
- Claims a union type can be used as an input type
- Says released GraphQL can express exactly-one in the schema
- Marks every alternative Non-Null so none can be omitted
- Leaves the rule only in a description string
- Assumes every server implements the draft @oneOf marking
- Treats removing an alternative as an additive change