JSON and GraphQL: How Queries Map to JSON Responses
GraphQL is usually pitched as an alternative to REST, but under the hood it's a thin, elegant layer over plain JSON: requests go up as JSON, responses come back as JSON, and the response's shape is a mirror of the query. Once you internalize that mapping, GraphQL responses — and their failure modes — become much easier to reason about.
Every GraphQL response is JSON
A GraphQL server over HTTP speaks exactly one response shape: a JSON object with up to three keys — data, errors, and extensions. There is no special encoding anywhere. What comes back is a JSON document you can JSON.parse, log, diff, or pipe through any generic JSON tooling you already own.
The request side is the same story: a POST whose body is JSON containing the query text, a variables object, and optionally an operationName. GraphQL defines the query language and execution semantics; JSON carries both halves. That's worth internalizing, because everything downstream — caches, mocks, contract tests, debug proxies — can treat GraphQL as JSON with a predictable envelope.
Responses mirror the query
The defining feature is shape mirroring: the data object has exactly the keys your selection set asked for, nested the way you nested them. Ask for a user's name and the first two of their posts' titles, and that is precisely the JSON you receive — nothing more, nothing less.
query UserPosts($id: ID!) {
user(id: $id) {
name
posts(first: 2) {
title
publishedAt
}
}
}{
"data": {
"user": {
"name": "Ada Lovelace",
"posts": [
{
"title": "Notes on the Analytical Engine",
"publishedAt": "2025-08-02"
},
{
"title": "Why JSON envelopes matter",
"publishedAt": "2025-08-16"
}
]
}
}
}This mirroring is how GraphQL solves over-fetching: the client designs the response document in advance. It also means a response is self-describing — you can reconstruct what was asked for (though not the arguments) from the JSON alone, which makes response fixtures excellent test material.
Variables travel as JSON
In practice, queries are written once with $ placeholders and sent repeatedly with different values. The variables field in the request body is where those values live — plain JSON, typed on the GraphQL side by the declaration like $id: ID! in the operation header.
{
"query": "query UserPosts($id: ID!) { user(id: $id) { name posts { title } } }",
"variables": { "id": "42" },
"operationName": "UserPosts"
}Splitting static query text from per-request data is what makes GraphQL documents cacheable and reusable: the string never changes between calls, only the JSON beside it. A common bug on the client side is inlining dynamic values into the query text with string interpolation — don't; variables exist precisely so servers can validate inputs instead of executing them.
Aliases and fragments change the JSON
Aliases rename keys in the response. Fetching the same field twice with different arguments requires them — userA: user(id: "1"), userB: user(id: "2") — and the aliases become literal keys in the JSON: an object with userA and userB. If you generate types from responses, aliases are part of the contract, not cosmetic.
Fragments, by contrast, are invisible in the response. They're a query-level construct that spreads its fields into the selection set before execution, so a fragment on User contributes its fields as if they'd been written inline. The consequence for tooling: you cannot know a response's shape by eyeballing the query until you've expanded its fragments — type generators always expand them first.
The errors array and partial data
GraphQL deliberately returns HTTP 200 with an errors array for field-level failures, because partial success is a first-class outcome: one branch of a query can fail while the rest resolves. Each error carries a message, typically path — the field that failed — and locations pointing at the query text; server-specific detail goes in extensions, an unrestricted JSON object.
The consumer contract is therefore: check errors first, then use whatever data is present. A null where you expected an array usually means that field errored while its parent resolved — data and errors describe the same response, not two different outcomes. Clients that assume every selection is non-null crash on exactly this case.
When GraphQL beats plain JSON REST
GraphQL earns its complexity when many clients need many different slices of one domain: mobile apps trimming payloads, dashboards composing data from several services behind one schema, public APIs you can't version per consumer. One typed schema plus introspection replaces a scatter of bespoke JSON endpoints and the guesswork of reading their docs.
It doesn't pay everywhere. A service with two consumers and three endpoints is simpler as JSON-over-REST. GraphQL's costs are concrete — N+1 resolver queries, query complexity limits, and losing the HTTP caching that REST GET endpoints get for free — and none of them shrink at small scale.
Common misconceptions
GraphQL is not a database or a storage format. It's a query layer over resolvers you write, and those resolvers may hit a database, five HTTP services, or a CSV file — the query language neither knows nor cares. "GraphQL is slow" and "GraphQL is fast" are both wrong in general; resolver strategy decides.
GraphQL responses are also not browser-cacheable by default, because the standard transport is POST and POST bodies aren't cache keys. Real deployments add cacheability with GET queries for cacheable reads, automatic persisted queries that map short ids to documents, or server-side response caching. Treat 'we use GraphQL, so caching is solved' as a smell, not a plan.
FAQ
- Is GraphQL just JSON?
- No — GraphQL is a query language and execution engine that uses JSON as its wire format on both sides. The request body is JSON wrapping a query string plus variables, and the response is JSON shaped to mirror the query. Take away the query language and you're left with an ordinary JSON-over-HTTP API; the query language is the product.
- Why do GraphQL errors come back with HTTP 200?
- Because the transport succeeded and a valid JSON document came back; the failure happened at the field level, and partial data plus errors is a normal GraphQL outcome. Most servers reserve non-200 statuses for transport and request-format problems — bad JSON, unknown operation. Clients must inspect the errors array rather than the status code.
- Can I send GraphQL queries with GET?
- Yes, for queries only: the query text goes in the URL's query string, which makes responses eligible for HTTP caching. Mutations must use POST since they have side effects and need a body for variables. Automatic persisted queries take this further by sending a short hash instead of the full query text.
- What is the extensions field in a GraphQL response?
- An escape hatch for server-defined data attached to the response or to individual errors: deprecation notices, tracing, cost accounting, feature flags. The spec puts no structure on it, so treat extensions as optional JSON your client should read defensively — useful diagnostics, never load-bearing state.
Try these tools
Related articles
What is JSON? A Complete Beginner's Guide →
JSON (JavaScript Object Notation) is the lightweight data-interchange format behind nearly every API, config file, and NoSQL database. Learn the syntax, the six data types, how parsing behaves in code, and the mistakes everyone makes hand-writing it.
The History of JSON: From 2000 to Industry Standard →
Trace JSON from Douglas Crockford's idea in 2001, through Yahoo's adoption, to becoming the ECMA-404 standard that powers 90% of modern APIs.
The Complete JSON Schema Guide (Draft 7) →
JSON Schema is the standard for describing and validating JSON structure. Learn the core keywords, build a real API schema, compose and reuse schemas with $ref, and run validation in code with Ajv.