Schema Design — Types, Nullability, Inputs & Evolution
A GraphQL schema is a versioned product contract. Nullability, input types, and additive evolution decide whether the next ship breaks clients or only adds a field they can ignore.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Question ladder
L1
What does the exclamation mark mean on a field?
Answer
The server promises a value. Null becomes an error instead of a JSON null.
L2
What happens when a non-null resolver returns null?
Answer
The error is recorded and the nearest nullable parent becomes null. If that parent is also non-null, the bubble walks up.
L3
When do you use an interface versus a union?
Answer
An interface when the members share fields. A union when the results are different shapes with no shared fields.
L4
Why a payload type instead of returning User directly?
Answer
You can add userErrors, an audit id, or a warning later without breaking clients that ignore unknown fields.
L5
How do you version the graph?
Answer
Add optional fields and optional arguments. Deprecate with a reason. Avoid a second GraphQL URL unless the organization forces a hard cut.
L6
What goes in userErrors versus the top-level errors array?
Answer
Expected domain failures, such as validation, go in the payload. Unexpected auth or infrastructure failures go in the top-level errors. Pick one convention and keep it.
L7
How do enums and lists evolve?
Answer
New enum values can surprise old clients. Document an unknown-value policy. A list of non-null items still needs a bound. Pagination mechanics stay in the API Design cluster.
Failure modes
Non-null on a value that can disappear
A later product change makes the field optional and every old parent nulls out.
Required new argument
Existing callers cannot send the new argument, so the mutation breaks.
In-place type change
String to Int, or a scalar to an object, breaks every selection set that already shipped.
Output types reused as inputs
Inputs cannot carry resolvers or circular output objects. The schema fails to compose.
Misconceptions
Non-null is always better client DX.
It is better only when the warranty is true. Otherwise the client loses the whole parent.
Field order in the schema is the response contract.
The selection set is the contract. Order is not.
GraphQL needs /v2 the same way REST does.
Prefer additive change. A hard cut is an organizational escape hatch, not the default.
Interviewer traps
Explaining REST URL versioning in detail.
One sentence, then the link to API Design. Stay on deprecation and optional arguments.
Treating protobuf field numbers as GraphQL names.
GraphQL identity is the field name. Protobuf identity is the number. Point at the gRPC cluster for wire rules.
Design scenario
Same prompt for every reader.
Requirements
Old clients keep working. New clients can show currency. Validation errors return without throwing away the whole payload.
Traffic / scale
The mutation is a small fraction of read traffic.
Latency
Schema checks run in CI, not in the request.
Consistency
price and priceV2 agree for one release, then price is deprecated.
Availability
A missing nickname must not null the user.
Failure assumptions
- Someone marks email non-null before anonymous traffic is gone.
- A new required argument ships on placeOrder.
Constraints
- Do not publish a second GraphQL path.
- Do not change price from Float to an object in place.
Prompt
The order screen needs a new money object. Today price is a Float. Old mobile builds are still in the store.
API
Which new fields are optional, and what does the payload contain?
Data
What is nullable on User, and what stays non-null forever?
Architecture
Where does schema diff CI sit relative to the deploy?
Overview
Schema design is not "TypeScript types on the server." REST can ship /v2/orders when a shape changes. GraphQL asks you to keep one graph and grow it. That bargain holds only if you:
- Do not remove or tighten fields casually.
- Add optional fields and optional arguments instead of required ones.
- Treat non-null as a promise you will keep.
Protobuf uses field numbers and reserved ranges. GraphQL uses names plus deprecation. Both need review. Neither gives you free compatibility. Wire details stay in the gRPC cluster.
Nullability
Flow
- 1
Resolver returns null
- nextField is Non-Null
- 2
Field is Non-Null
- nextError joins errors array
- 3
Error joins errors array
- nextNearest nullable parent nulls
- 4
Nearest nullable parent nulls
- nextWalk up while parent is Non-Null
- 5
Walk up while parent is Non-Null
- nextClient sees a data hole
- 6
Client sees a data hole
Lesson map
Schema Design — Types, Nullability, Inputs & Evolution
A GraphQL schema is a versioned product contract. Nullability, input types, and additive evolution decide whether the next ship breaks clients or only adds a field they can ignore.
Architecture. Architecture
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB a["Resolver returns null"] b["Field is Non-Null"] c["Error joins errors array"] d["Nearest nullable parent nulls"] a -->|Resolver returns null| b b -->|Field is Non-Null| c c -->|Error joins errors array| d
Default to nullable unless product and storage always return the value for the life of the API.
Lists need two decisions. A non-null list of non-null items means the list itself is present and no element is null. That is a strong promise. A nullable element is how you represent a missing row without killing the list.
How strict to make the type
Prefer
Nullable unless the warranty is real
Clients check null on purpose. A missing nickname does not blank the user card.
- Migrations that make a field optional stay safe.
- Partial data still renders.
- Generated clients have more null checks. That is the point.
Alternative
Non-null everywhere
Cleaner call sites until the first hole. Then the bubble deletes a whole subtree.
- One database miss nulls a parent that was also non-null.
- Tightening a field later is a breaking change.
- Use it for ids and other true invariants only.
What the runtime does with null
- 1
Resolver returns null
The field has no value for this parent. - 2
Type says non-null
Null is not a legal JSON value for that field. - 3
Error is recorded
The errors array gains an entry with the path. - 4
Parent becomes null
The walk stops at the first nullable ancestor.
Types you actually choose
- Object types are the nodes clients select: User, Order.
- Enums are closed sets you control. Leave a documented path for unknown values on read, because old clients may not know a new case.
- Interfaces share fields across implementations.
Nodewith an id is the usual example. - Unions are heterogeneous results with no shared fields. Search might return a user, an order, or an article.
- Input types group mutation arguments and complex query args. They cannot contain output-only types.
- Custom scalars such as DateTime or Money need a written serialization. Otherwise every client invents a format.
Mutation shape
Prefer an input object and a payload object.
type Mutation {
updateEmail(input: UpdateEmailInput!): UpdateEmailPayload!
}
input UpdateEmailInput {
userId: ID!
email: String!
idempotencyKey: String
}
type UpdateEmailPayload {
user: User
userErrors: [UserError!]!
}
type UserError {
field: [String!]
message: String!
}Returning User directly feels smaller until you need userErrors or an audit id. Clients that ignore unknown fields survive an additive payload. Expected validation belongs in userErrors. Surprises such as auth or a downed dependency belong in the top-level errors array. Write that down once.
Put idempotencyKey on the input when a retry must not double-apply. The store itself is idempotency keys.
Evolution playbook
- Add optional fields and optional arguments.
- Deprecate with a reason before removal. Give clients a release to move.
- Never change a field type in place. String to Int is a break. Float price to an object is a break.
- Prefer a new field, such as a money object, over mutating the old scalar.
- Run a schema diff in CI (GraphQL Inspector, Rover, or the same idea in your review bot).
| Concern | GraphQL | REST JSON | Protobuf |
|---|---|---|---|
| Add a field | Usually safe | Usually safe | Safe with a new field number |
| Remove a field | Break unless unused | Break | Reserve the number |
| Rename | Break | Break | New field, deprecate the old number |
| New required input | Breaks old callers | Breaks old callers | Required on the wire breaks old senders |
| Null versus absent | The type says which | Often informal | Presence and defaults differ by version |
URL and header versioning tactics stay in API Design. Do not re-teach them here. If the company already mandates a hard cut, say so, then still evolve inside that cut with the rules above.
Sandbox: reject a bad input before the database
Press Run. Snippets must be self-contained — no network, files, or native modules.
Press Run. Snippets must be self-contained — no network, files, or native modules.
Pitfalls
Take User with id, email, and nickname. Which fields are non-null for the life of the API? What happens if nickname's resolver returns null and the user field is non-null?
Interview Q&A
Why is a non-null string riskier than a nullable string?
Answer
Null from the resolver becomes a GraphQL error and can null parent objects. Clients that assumed the parent was always present blank a whole card. See the bubble above.
Input type versus loose arguments?
Answer
An input groups fields, evolves as one object, and is the reusable mutation shape. Loose arguments get noisy and are harder to share across fields.
Interface versus union?
Answer
Interface when members share fields the client can select without a fragment for each case. Union when the results share nothing but a slot in the response.
How do you version GraphQL?
Answer
Continuous evolution and deprecation. Avoid a second GraphQL path unless the organization mandates a hard cut. REST URL versions are a different cluster: API Design.
What belongs in userErrors versus thrown errors?
Answer
Expected domain failures (validation, conflict) go on the payload. Infrastructure and auth surprises go in the top-level errors array. Write the convention down so clients do not guess.
Can clients rely on field order?
Answer
No. The selection set defines the shape. Order is not a contract.
How do enums evolve?
Answer
Adding a value is source-compatible for the schema and still a runtime surprise for clients that switch on every case. Document whether clients must ignore unknown values.
Where does idempotency live on the schema?
Answer
On the mutation input, as a client-generated key. Dedupe storage is idempotency keys, not this page.