The Response-Key Collision
The Response-Key Collision
You need two users in one round trip — the entire point of GraphQL — so you write:
query Both {
user(id: "1") { name }
user(id: "2") { name }
}
The server answers before executing anything:
Fields "user" conflict because "id" arguments differ.
Use different aliases on fields to fetch both.
Inside one selection set every response key must be unique, and the default response key is the field's name. Two selections named user with different arguments are two different answers competing for a single slot in data, so the document fails validation rather than guessing which one wins.
An alias renames the response key without touching the schema:
query Both {
viewer: user(id: "1") { name }
target: user(id: "2") { name }
}
{ "data": { "viewer": { "name": "Ada" }, "target": { "name": "Grace" } } }
The schema still exposes one user field with one id argument. The caller simply asked for it twice under two names — no server change, no new field, no version bump.
Note what an alias is not: an argument. user(id: "1") and user(id: "2") still differ at the argument level; the alias only stops them from colliding in the response. When two selections use the same field and the same arguments, they merge instead, which is why duplicating an identical selection is legal (and pointless).
Aliases also decide what client code binds to. recentPosts as a response key documents intent in the JSON, where a bare posts forces every reader to inspect the arguments to learn which list they are holding. GraphQL drew this line deliberately: field names belong to the schema and must stay stable for every client, while response keys belong to the document and can vary per consumer with no negotiation, no versioning, and no server deploy.
Mapping Aliases to Response Keys
Mapping Aliases to Response Keys
The alias becomes the key in data at every level where you use one:
query Dashboard {
recent: posts(limit: 3, orderBy: NEWEST) { title }
popular: posts(limit: 3, orderBy: TOP) { title }
me {
displayName: name
inbox: messages(filter: UNREAD) { subject }
}
}
| Position in the document | Response key |
|---|---|
recent: posts(...) |
data.recent |
popular: posts(...) |
data.popular |
displayName: name |
data.me.displayName |
inbox: messages(...) |
data.me.inbox |
Rules worth memorising:
- An alias renames the result, never the field. Resolvers still run against
postsandmessages; field names, types, and deprecations stay exactly as the schema declares them. - Aliases compose with variables.
recent: posts(limit: $n)is legal — the alias is fixed in the document while the value arrives separately. See query variables for how$nis delivered and validated. - You cannot alias a fragment spread.
...Card: useris not syntax. To rename a spread selection, put the fragment on a field:viewer: user { ...UserCard }— the alias lands onuser, the fragment's fields merge underneath it. - Two aliases with identical arguments are legal but wasteful. The resolver executes twice for identical data; nothing merges them because their response keys differ.
The merge rule covers every such case: two selections of one field with different arguments conflict, no matter how small the difference — a limit, an order, an id — and aliasing is the fix each time.
Aliases nest like the fields they rename: stats: user(id: "1") { recent: posts(limit: 3) { title } } renames keys at two levels in one document, which is how a dashboard fetches several time-windowed versions of the same list without colliding at either level.
A naming convention keeps documents readable as they grow: nouns for entities (viewer, target), scope prefixes for variants (recent, popular). Response keys appear in every network trace and every cache read, so inconsistency costs readers time long after the query ships.
Aliases, Normalized Caches, and Read Paths
Aliases, Normalized Caches, and Read Paths
In a normalized client cache (Apollo Client, urql) records are stored by __typename plus id, while query results are addressed by response key:
document: viewer: user(id:"1") target: user(id:"2")
store: User:1 ← read via data.viewer User:2 ← read via data.target
Three production consequences follow:
- The alias is a read path. Result resolvers read
data.viewer, so renaming an alias breaks everycache.readQueryand test that expects the old key. Treat alias names like a public interface of the document. - Documents that differ only in aliases are different documents. Two dashboards fetching the same records through different alias names create two cache entries and two network requests — aliases can fragment the operation cache exactly as embedded literals do. See fragment reuse for the same concern applied to shared selection sets.
- Aliases are the client's private namespace. Because the schema does not change, aliases need no coordination, no deprecation cycle, and no server deployment — which is why "fetch the same field twice with different arguments" should be answered with an alias rather than a near-duplicate schema field.
What aliases cannot do is conditionally rename: a response key is fixed by the document, so switching which key appears means a different document. For conditional presence of a field, see include and skip directives.
Interview angle: asked "how do you fetch two users in one query?", give the alias answer first, show the response-key mapping, then mention the alternative — a list argument like users(ids: [ID!]) when the server can batch — and note that the collision is a validation error, caught before execution.
Practice Problems
A draft query fetches two users and fails validation. Diagnose the error, fix it with aliases, and write out the response keys the client will read. Then add a second alias that fetches the three newest posts alongside the users.
Example:
Input: query Both { user(id: "1") { name } user(id: "2") { name } }
Output: Fields "user" conflict because "id" arguments differ. Fixed: query Both { viewer: user(id: "1") { name } target: user(id: "2") { name } latest: posts(limit: 3, orderBy: NEWEST) { title } } Response keys: data.viewer, data.target, data.latest
Response keys must be unique inside a selection set, and the default key is the field name. The alias renames only the JSON key — execution still targets the user and posts fields declared by the schema.
Optimal Solution — O(1) time, O(r) space
Alias each colliding selection; the response key becomes the read path while the resolver keeps seeing the schema field name.
query Both {
viewer: user(id: "1") { id name }
target: user(id: "2") { id name }
latest: posts(limit: 3, orderBy: NEWEST) { id title }
}
// {
// "data": {
// "viewer": { "id": "1", "name": "Ada" },
// "target": { "id": "2", "name": "Grace" },
// "latest": [ ... ]
// }
// } Edge Cases:
- Both aliases pointing at the same id — legal, but the resolver runs twice for identical data
- A nullable user returning null still appears under its alias key: data.target: null
- Aliasing a fragment spread is invalid syntax — alias the field and spread inside it
Quiz
1. Two selections of user(id:) with different ids fail validation. What is the fix?
2. For the selection displayName: name inside me, what does the resolver receive and where does the value appear?
3. Can a fragment spread be aliased directly, as in ...Card: user?
4. Why do two documents differing only in alias names behave like two different queries to a client cache?
Flashcards
Question
Response key rule
Click to reveal answer
Answer
Each response key in a selection set must be unique; the default key is the field name, an alias overrides it.
Question
What an alias changes
Click to reveal answer
Answer
Only the key in the JSON response. Resolvers, field names, types, and the schema stay identical.
Question
Same field, two arguments
Click to reveal answer
Answer
Not mergeable — validation fails with a field conflict error. Alias one of them to give each a key.
Question
Aliasing a fragment spread
Click to reveal answer
Answer
Not syntax. Wrap: viewer: user { ...UserCard } — alias the field, merge the fragment under it.
Question
Alias naming discipline
Click to reveal answer
Answer
Response keys are read paths in the client cache; renaming an alias breaks readQuery calls and tests.
Revision Notes
Key Takeaways
- 1. Aliases rename response keys, letting one document fetch the same field repeatedly with different arguments
- 2. Response-key collisions are validation errors, caught before any resolver runs
- 3. Aliases never alter execution: resolvers still see the schema field name
- 4. Normalized caches read by response key, so alias names are effectively public API of the document
Interview Tips
- • Answer "fetch two users in one query" with aliases, then show the exact response keys you expect
- • Explain the difference between merging (same field and arguments) and conflicting (same field, different arguments)
- • Discuss cache consequences: alias names are read paths, and differing alias sets fragment the operation cache
Cheat Sheet
Field Aliases Cheat Sheet
query {
viewer: user(id: "1") { name } # data.viewer
target: user(id: "2") { name } # data.target
recent: posts(limit: 3, orderBy: NEWEST) { title } # data.recent
popular: posts(limit: 3, orderBy: TOP) { title } # data.popular
}
- Syntax:
alias: fieldName(args) { … }— any level of any selection - Default response key = field name; alias overrides it in JSON only
- Same field + different args ⇒ conflict error; alias fixes it
- Same field + same args ⇒ merged (duplication is legal, not useful)
- No aliasing spreads:
viewer: user { ...UserCard } - Response keys are client read paths — renaming them breaks cache reads