JSON and GraphQL: How Queries Map to JSON Responses
People pitch GraphQL as an alternative to REST, but it sits as a thin layer over plain JSON: requests go up as JSON, responses come back as JSON, and the response's shape mirrors the query. Once you learn that mapping, GraphQL responses and their failure modes get much easier to reason about.
Every GraphQL response is JSON
A GraphQL server over HTTP returns one response shape: a JSON object with up to three keys, data, errors, and extensions. It uses no special encoding. You get a JSON document you can JSON.parse, log, diff, or pipe through any JSON tooling you already own.
The request side works the same way: a POST whose JSON body contains the query text, a variables object, and an optional operationName. GraphQL defines the query language and execution semantics, and JSON carries both halves. Keep that in mind, because everything downstream (caches, mocks, contract tests, debug proxies) can treat GraphQL as JSON with a predictable envelope.
Responses mirror the query
Shape mirroring defines GraphQL: the data object has the keys your selection set asked for, nested the way you nested them. Ask for a user's name and the titles of their first two posts, and you receive that JSON and nothing else.
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"
}
]
}
}
}Mirroring is how GraphQL solves over-fetching: the client designs the response document in advance. It also makes a response self-describing. You can reconstruct what the client asked for (though not the arguments) from the JSON alone, which makes response fixtures excellent test material.
Variables travel as JSON
You write queries once with $ placeholders and send them repeatedly with different values. Those values go in the variables field of the request body as plain JSON, typed on the GraphQL side by a 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 makes GraphQL documents cacheable and reusable: the string stays the same between calls, and only the JSON beside it changes. A common client-side bug inlines dynamic values into the query text with string interpolation. Don't. Variables exist so servers can validate inputs instead of executing them.
Aliases and fragments change the JSON
Aliases rename keys in the response. You need them to fetch the same field twice with different arguments (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, treat aliases as part of the contract.
Fragments leave no trace in the response. The server spreads a fragment's fields into the selection set before execution, so a fragment on User contributes its fields as if you'd written them inline. For tooling, that means you can't read a response's shape off the query until you've expanded its fragments, and type generators expand them first.
The errors array and partial data
GraphQL returns HTTP 200 with an errors array for field-level failures on purpose, because it treats partial success as a normal outcome: one branch of a query can fail while the rest resolves. Each error carries a message, usually a path (the field that failed), and locations pointing at the query text. Server-specific detail goes in extensions, an unrestricted JSON object.
So the consumer contract reads: 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 this case.
When GraphQL beats plain JSON REST
GraphQL earns its complexity when many clients need 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 stays simpler as JSON-over-REST. GraphQL brings concrete costs (N+1 resolver queries, query complexity limits, and the loss of the HTTP caching REST GET endpoints get for free), and none of them shrink at small scale.
Common misconceptions
GraphQL is a query layer over resolvers you write, not a database or a storage format. Those resolvers may hit a database, five HTTP services, or a CSV file, and the query language neither knows nor cares. Claims that "GraphQL is slow" or "GraphQL is fast" miss the point; your resolver strategy decides.
Browsers don't cache GraphQL responses by default, because the standard transport is POST and POST bodies aren't cache keys. Real deployments add caching with GET queries for cacheable reads, automatic persisted queries that map short ids to documents, or server-side response caching. If someone says 'we use GraphQL, so caching is solved', treat it as a warning sign.
FAQ
- Is GraphQL only 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. Remove the query language and you have an ordinary JSON-over-HTTP API, so the query language is what you're buying.
- Why do GraphQL errors come back with HTTP 200?
- The transport succeeded and a valid JSON document came back. The failure happened at the field level, and GraphQL treats partial data plus errors as a normal outcome. Most servers reserve non-200 statuses for transport and request-format problems such as bad JSON or an unknown operation. Clients must inspect the errors array instead of 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 lets HTTP caches store responses. Mutations must use POST, since they have side effects and need a body for variables. Automatic persisted queries go further by sending a short hash instead of the full query text.
- What is the extensions field in a GraphQL response?
- The server uses it to attach its own data to the response or to individual errors: deprecation notices, tracing, cost accounting, feature flags. The spec puts no structure on it, so your client should read extensions defensively as optional JSON. Use it for diagnostics and never depend on it for 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.