JSON to GraphQL: Generate Types, Queries, and Schema from JSON
GraphQL and JSON are complementary technologies — GraphQL queries request fields by name, and GraphQL responses are always valid JSON. Converting between them means two things: deriving GraphQL type definitions from a JSON structure, and fetching or returning GraphQL data as JSON. A JSON object {"id":1,"name":"Alice","email":"alice@example.com"} maps to a GraphQL type type User { id: Int!, name: String!, email: String! }. Nullable fields use the base type without !; arrays use bracket notation [String]. Tools like graphql-codegen and json-to-graphql-query automate the conversion. GraphQL responses always use {"data":{...},"errors":[...]} envelope, so JSON parsing is straightforward: const result = await response.json(); const users = result.data.users. The graphql-request package provides a typed client for sending queries and receiving JSON responses with TypeScript inference. This guide covers JSON-to-GraphQL type mapping, query structure, graphql-request, graphql-codegen, and comparing GraphQL JSON responses with REST API JSON responses.
Need to inspect or pretty-print a GraphQL JSON response? Jsonic's formatter handles it instantly.
Open JSON FormatterJSON to GraphQL Type Mapping
The fastest way to convert a JSON object to a GraphQL schema is to map each JSON value type to its GraphQL scalar equivalent. JSON has 6 value types; GraphQL has 5 built-in scalars plus the ID type — knowing the 1-to-1 mapping lets you derive a type definition in under 60 seconds for any JSON object.
The type mapping rules are: string maps to String, number maps to Int (integer) or Float (decimal), boolean maps to Boolean, null or a missing field maps to a nullable type (no !), a nested object maps to a new named type, and an array maps to bracket notation [Type]. Fields that are always present and never null get the non-null modifier !.
Consider a 5-field JSON object representing a user:
// JSON input
{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"score": 98.5,
"active": true
}
// Equivalent GraphQL type definition
type User {
id: ID!
name: String!
email: String!
score: Float!
active: Boolean!
}Note that id uses the ID scalar — not Int! — because the field serves as an identifier. The ID scalar is serialized as a string in JSON even when the value is a number, which ensures consistent handling across all 3 GraphQL transports (HTTP, WebSocket, subscriptions).
Nullable fields: if a field such as bio is sometimes null and sometimes a string, map it to String (no !). Nested objects become separate type definitions linked by field name:
// JSON with nested object
{
"id": 1,
"name": "Alice",
"address": {
"city": "London",
"country": "UK"
}
}
// GraphQL — nested object becomes a separate type
type Address {
city: String!
country: String!
}
type User {
id: ID!
name: String!
address: Address!
}Arrays use bracket notation: a JSON array of strings maps to [String]; an array of objects maps to [TypeName]. A non-null array of non-null items is [Type!]! — the outer ! means the array itself cannot be null, the inner ! means no item in the array can be null. Use JSON Schema to formally document nullability before mapping to GraphQL.
GraphQL Query Structure as JSON Input
Every GraphQL request is sent over HTTP as a JSON POST body — the query itself is a string value inside a JSON object. Understanding this 3-field structure unlocks direct integration with any HTTP client, including fetch, curl, and server-side request libraries, without a dedicated GraphQL client.
A GraphQL HTTP request has 3 optional keys: query (the operation string), variables (a JSON object of variable values), and operationName (a string selecting one named operation when a document contains multiple). Only query is required. The request must use the POST method and set Content-Type: application/json:
// Minimal GraphQL request body
{ "query": "query { users { id name } }" }
// With variables — operationName is optional for named queries
{
"query": "query GetUsers($limit: Int!) { users(limit: $limit) { id name email } }",
"variables": { "limit": 10 },
"operationName": "GetUsers"
}
// Send with fetch
const response = await fetch('https://api.example.com/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `query GetUsers($limit: Int!) {
users(limit: $limit) { id name email }
}`,
variables: { limit: 10 },
}),
})
const result = await response.json()
// result.data.users → [{ id: "1", name: "Alice", email: "alice@example.com" }]
// result.errors → undefined (on success) or [{ message: "..." }]The response is always JSON with 2 top-level keys: data holds the query result, and errors holds an array of error objects if any field resolvers failed. Both can be present simultaneously — a partial response with some data and some errors is valid in the GraphQL spec. Always check if (result.errors) before accessing result.data.
For a mutation (write operation), use the same JSON structure but replace query with a mutation keyword in the operation string:
// Mutation request body
{
"query": "mutation CreateUser($name: String!, $email: String!) { createUser(name: $name, email: $email) { id name } }",
"variables": { "name": "Bob", "email": "bob@example.com" }
}
// Success response
{ "data": { "createUser": { "id": "42", "name": "Bob" } } }
// Error response (HTTP 200, check errors array)
{ "errors": [{ "message": "email already exists", "locations": [{"line":1,"column":51}] }] }Use Jsonic's JSON formatter to pretty-print and inspect GraphQL response bodies during development. Compare REST API JSON responses to understand how GraphQL's single-endpoint approach differs from REST's resource-per-URL model.
Fetch GraphQL with graphql-request
graphql-request is the smallest typed GraphQL client at 5.4 kB gzipped — it wraps the JSON POST logic shown above into a 2-line API with TypeScript inference. It ships no cache, no reactive layer, and no peer dependencies beyond graphql, making it ideal for server-side fetching, Next.js Server Components, and scripts.
npm install graphql-request graphqlimport { GraphQLClient, gql } from 'graphql-request'
// 1. Create a client pointed at your GraphQL endpoint
const client = new GraphQLClient('https://api.example.com/graphql', {
headers: {
Authorization: 'Bearer YOUR_TOKEN',
},
})
// 2. Define the query with the gql tagged template literal
const GET_USERS = gql`
query GetUsers($limit: Int!) {
users(limit: $limit) {
id
name
email
}
}
`
// 3. Execute the query — returns data directly (throws on errors)
const data = await client.request(GET_USERS, { limit: 10 })
console.log(data.users) // [{ id: "1", name: "Alice", email: "..." }]TypeScript inference: pass a generic type to client.request to get fully typed results. This is the pattern used with JSON to TypeScript type generation — define the response shape once and get autocomplete everywhere:
// Define the response type (or generate with graphql-codegen)
type UsersQuery = {
users: Array<{
id: string
name: string
email: string
}>
}
// Pass the generic — data is fully typed
const data = await client.request<UsersQuery>(GET_USERS, { limit: 10 })
data.users[0].name // TypeScript: string ✓
data.users[0].typo // TypeScript: error ✗For one-off requests without a persistent client, use the exported request function directly:
import { request, gql } from 'graphql-request'
const data = await request(
'https://api.example.com/graphql',
gql`query { users { id name } }`
)
// Error handling — graphql-request throws on GraphQL errors
try {
const data = await client.request<UsersQuery>(GET_USERS, { limit: 10 })
} catch (error) {
if (error instanceof ClientError) {
console.error(error.response.errors) // GraphQL errors array
console.error(error.response.status) // HTTP status code
}
}Compared to Apollo Client at 32 kB, graphql-request at 5.4 kB is 6x smaller with no client-side cache or reactive layer. For server-side use cases where you control data freshness yourself (ISR, Redis cache, SWR), graphql-request is the correct choice. Use Apollo Client only when you need normalized client-side caching and reactive re-rendering in a React application.
Generate TypeScript from JSON Schema with graphql-codegen
graphql-codegen generates TypeScript interfaces directly from a GraphQL schema file, eliminating hand-written type definitions. It reads your .graphql schema file and outputs .ts types in under 1 second — keeping TypeScript types always in sync with the schema without manual effort.
npm install --save-dev @graphql-codegen/cli @graphql-codegen/typescriptCreate a codegen.ts configuration file at the project root:
// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
schema: 'schema.graphql', // path to your GraphQL schema file
generates: {
'src/generated/types.ts': { // output file
plugins: ['typescript'], // generate TypeScript interfaces
},
'src/generated/operations.ts': {
documents: 'src/**/*.graphql', // scan for .graphql query files
plugins: [
'typescript',
'typescript-operations', // generate types for each query/mutation
],
},
},
}
export default config# Run codegen — generates types.ts from schema.graphql
npx graphql-codegen
# Watch mode — regenerates on every schema change
npx graphql-codegen --watchGiven a schema.graphql with the User type from Section 1, codegen outputs:
// src/generated/types.ts — auto-generated, do not edit
export type Scalars = {
ID: { input: string; output: string }
String: { input: string; output: string }
Boolean: { input: boolean; output: boolean }
Int: { input: number; output: number }
Float: { input: number; output: number }
}
export type User = {
__typename?: 'User'
id: Scalars['ID']['output']
name: Scalars['String']['output']
email: Scalars['String']['output']
score: Scalars['Float']['output']
active: Scalars['Boolean']['output']
}For the reverse direction (JSON Schema to GraphQL), the graphql-codegen-json-schema plugin converts a JSON Schema $def to a GraphQL type. Each $def in the JSON Schema becomes a GraphQL type; JSON Schema required fields map to non-null (!) GraphQL fields;nullable: true maps to a nullable field. This is the most reliable automated path from JSON Schema to a usable GraphQL schema.
Use JSON to TypeScript type generation for REST APIs — the workflow is similar but targets plain TypeScript interfaces rather than GraphQL types.
GraphQL vs REST JSON Responses
GraphQL and REST both return JSON, but the structure differs in 4 key ways: field selection, endpoint design, error representation, and the N+1 query problem. Understanding these differences helps when migrating a REST JSON API to GraphQL or wrapping REST behind a GraphQL layer.
Side-by-side comparison for the same "get user with posts" data:
// REST: requires 2 round trips, returns full objects
// GET /users/1
{ "id": 1, "name": "Alice", "email": "alice@example.com", "age": 30, "createdAt": "..." }
// GET /users/1/posts
[
{ "id": 10, "title": "Hello World", "body": "...", "createdAt": "...", "updatedAt": "..." },
{ "id": 11, "title": "GraphQL tips", "body": "...", "createdAt": "...", "updatedAt": "..." }
]
// GraphQL: 1 round trip, returns only requested fields
// POST /graphql with body: { "query": "{ user(id:1) { name posts { title } } }" }
{
"data": {
"user": {
"name": "Alice",
"posts": [
{ "title": "Hello World" },
{ "title": "GraphQL tips" }
]
}
}
}Key differences across 5 dimensions:
| REST | GraphQL | |
|---|---|---|
| Endpoints | 1 URL per resource | Single /graphql endpoint |
| Field selection | Returns full resource | Returns only requested fields |
| Errors | HTTP status codes (404, 500) | Always HTTP 200; errors in errors array |
| Over-fetching | Common (full object returned) | Eliminated (only requested fields) |
| N+1 problem | Controlled by API design | Requires DataLoader for batching |
The GraphQL N+1 problem: fetching a list of 10 users and their posts triggers 1 query for users + 10 queries for posts (N+1 = 11 total). Use the dataloader package to batch and deduplicate these into 2 queries:
import DataLoader from 'dataloader'
// Batch function: called once with all collected IDs
const postLoader = new DataLoader(async (userIds: readonly string[]) => {
const posts = await db.posts.findByUserIds(userIds)
// Map results back to input order
return userIds.map((id) => posts.filter((p) => p.userId === id))
})
// Resolver — loader deduplicates and batches calls
const resolvers = {
User: {
posts: (user) => postLoader.load(user.id), // called for each user, batched
},
}When deciding between REST and GraphQL, consider: GraphQL adds schema definition and resolver complexity upfront but eliminates over/under-fetching long-term. REST is simpler to cache at the HTTP layer (CDN caching by URL) but requires versioning endpoints as requirements change. See REST API JSON response best practices for REST-specific guidance.
Key definitions
- GraphQL scalar
- A GraphQL scalar is a primitive leaf type that resolves to a single value — the 5 built-in scalars are
String,Int,Float,Boolean, andID, each of which maps to a corresponding JSON value type in responses. - Non-null modifier (
!) - The non-null modifier in GraphQL is the exclamation mark (
!) appended to a type, indicating the field is guaranteed never to benull— the absence of!means the field is nullable and the server may returnnullfor it. - graphql-codegen
graphql-codegenis a code generation tool that reads a GraphQL schema file and outputs TypeScript interfaces, React hooks, and query types, keeping generated types automatically in sync with the schema without manual editing.- graphql-request
graphql-requestis a minimal GraphQL HTTP client at 5.4 kB gzipped that sends queries as JSON POST requests and returns typed responses, with no client-side cache or peer dependencies beyond thegraphqlpackage.- DataLoader
- DataLoader is a batching and caching utility for GraphQL resolvers that collects all field-level data requests made during a single tick of the event loop and issues them as a single batched database query, solving the N+1 query problem.
- GraphQL variables
- GraphQL variables are a JSON object sent alongside a query that replaces inline literal values, allowing the same query string to be reused with different inputs while keeping dynamic values out of the query string for security and caching.
- Schema stitching
- Schema stitching is a technique for combining multiple GraphQL schemas — or REST JSON APIs wrapped in GraphQL resolvers — into a single unified GraphQL API endpoint, enabling clients to query data from multiple backends with one request.
Frequently asked questions
How do I convert a JSON object to a GraphQL type definition?
To convert a JSON object to a GraphQL type definition, map each JSON key to a GraphQL field with the corresponding scalar type: string maps to String!, number maps to Int! or Float!, boolean maps to Boolean!, null maps to a nullable type (no !), an array maps to [ItemType], and a nested object maps to a new named type. Name the type in PascalCase. For example, {"id":1,"name":"Alice","email":"alice@example.com"} maps to type User { id: ID!, name: String!, email: String! }. Use the ID scalar instead of Int when a field serves as an identifier. Tools like graphql-inspector or online converters automate this. When deriving types from sample data, be conservative — use nullable unless you are certain a field is always present.
What does a GraphQL request look like in JSON?
A GraphQL request is a POST with Content-Type: application/json and a JSON body containing a query field and an optional variables field. For example: {"query":"query GetUser { user(id:1) { name email } }","variables":{"id":1}}. The variables key is optional and holds a JSON object of variable values. The response is always JSON: {"data":{"user":{"name":"Alice","email":"alice@example.com"}}} on success, or {"errors":[{"message":"..."}]} on failure. Always check the errors array before accessing data. See GraphQL JSON responses for a deeper look at the full response structure including partial errors.
Why does GraphQL always return HTTP 200 even for errors?
The GraphQL spec says the HTTP response status represents the transport layer, not the query result. A query with errors still has a parseable JSON response body, so 200 is returned. The errors array in the response body carries error details including message, locations, and path fields. Use if (result.errors) to check before accessing result.data. This is fundamentally different from REST API JSON responses where HTTP status codes like 404 or 500 indicate the error type. Some GraphQL servers do return 4xx codes for transport-level errors (malformed JSON, missing query field), but this behavior is implementation-specific, not required by the spec.
What is the difference between graphql-request and Apollo Client?
graphql-request is a minimal GraphQL HTTP client at 5.4 kB gzipped that sends queries and returns JSON responses with TypeScript inference. Apollo Client at 32 kB adds a normalized client-side cache, reactive queries with re-rendering on cache updates, pagination helpers, and browser DevTools. Use graphql-request for server-side fetching or simple clients — it is 6x smaller and has no peer dependencies beyond graphql. Use Apollo Client when you need client-side caching and reactivity in a React application. For most server-side use cases in Next.js or Node.js, graphql-request is the right choice. For comparison, see how JSON to TypeScript type generation integrates with both clients via graphql-codegen.
How do I handle null fields when converting JSON to GraphQL?
Null values in JSON indicate optional or nullable fields. In GraphQL, a field without ! is nullable — the server may return null for it. If a JSON value is sometimes null and sometimes a string, map it to String (nullable, no !). If it is always present and non-null, use String!. When deriving types from sample JSON data, be conservative — use nullable unless you are certain a field is always present in every response. Missing keys in JSON should also be treated as nullable. For arrays that may be empty but never null, use [Type!]!; for arrays that may themselves be null, use [Type]. Use a JSON Schema with required to formally document nullability before mapping to GraphQL.
Can I use GraphQL to query a JSON REST API?
Not directly — GraphQL requires a server-side resolver layer. However, you can wrap a REST JSON API with a GraphQL schema by writing resolvers that fetch from REST endpoints and transform JSON responses into the expected GraphQL shape. The @graphql-tools/url-loader and @graphql-tools/wrap packages support schema stitching, combining multiple REST APIs behind a single GraphQL endpoint. Each GraphQL field resolver calls the appropriate REST endpoint and maps the JSON response. This lets you use GraphQL clients and type safety while keeping existing REST backends unchanged. Schema stitching can also combine multiple microservices into one unified GraphQL API.
Ready to work with GraphQL JSON responses?
Use Jsonic's JSON Formatter to validate and pretty-print GraphQL response bodies during development. You can also diff two JSON responses to compare GraphQL and REST responses for the same data.
Open JSON Formatter