JSON:API Specification: Resource Objects, Relationships & Compound Documents
JSON:API is a specification (jsonapi.org, current version 1.1) for structuring REST API responses — defining a standard envelope with data, included, meta, links, and errors top-level members so clients don't need per-API parsing logic. Every resource object requires exactly 2 members: type (string) and id (string — numeric IDs must be stringified). A relationships object links to related resources by type/id without embedding the full object, reducing payload size by 40–60% for nested data compared to naive inline embedding. The content type application/vnd.api+json is required on both request and response. This guide covers the complete resource object structure, compound documents and sideloading, sparse fieldsets, query parameters for filtering/sorting/pagination, error objects, and a side-by-side comparison with plain REST JSON responses and GraphQL. Before sending any JSON:API payload, use Jsonic to validate and prettify your JSON.
Need to inspect or validate a JSON:API response? Jsonic's JSON formatter parses and prettifies any JSON payload instantly.
Open JSON FormatterJSON:API document structure — the top-level envelope
Every JSON:API response is a JSON object (the "document") with at least 1 of 3 required top-level members: data, errors, or meta. A document MUST NOT contain both data and errors simultaneously — success and error responses use different shapes. The 5 possible top-level members are:
{
"data": { ... }, // primary resource object or array — REQUIRED (or errors/meta)
"errors": [ ... ], // array of error objects — REQUIRED (or data/meta); mutually exclusive with data
"meta": { ... }, // non-standard metadata — can appear alone or alongside data
"included": [ ... ], // sideloaded related resources (compound document); only with data
"links": { // pagination/self links
"self": "https://api.example.com/articles",
"next": "https://api.example.com/articles?page[offset]=10",
"prev": null
},
"jsonapi": { // server's JSON:API version info (optional)
"version": "1.1"
}
}The data member is either a single resource object (for single-resource endpoints like GET /articles/1) or an array of resource objects (for collection endpoints like GET /articles). An empty collection is "data": [] — never null. A missing resource is "data": null — never an empty object.
Here is a minimal valid single-resource document with 3 top-level members:
{
"data": {
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON:API: Why You Should Standardize Your REST Responses",
"body": "REST APIs have a well-known problem...",
"created-at": "2026-05-12T10:00:00Z"
}
},
"meta": {
"total-count": 1
},
"links": {
"self": "https://api.example.com/articles/1"
}
}Resource object structure — type, id, attributes, relationships
A resource object is the atomic unit of JSON:API. It must contain exactly 2 required members — type and id — and may contain 4 optional members:attributes, relationships, links, and meta. Understanding this structure is the foundation for building any JSON:API client or server.
{
"type": "articles", // REQUIRED — string, identifies the resource type
"id": "1", // REQUIRED — string; numeric DB IDs must be stringified
"attributes": { // the resource's data fields
"title": "JSON:API Guide",
"word-count": 3200,
"published": true
// MUST NOT contain "type", "id", or "relationships"
},
"relationships": { // links to related resources by type/id
"author": {
"links": { "related": "/articles/1/author" },
"data": { "type": "people", "id": "9" } // to-one resource linkage
},
"tags": {
"data": [ // to-many resource linkage
{ "type": "tags", "id": "2" },
{ "type": "tags", "id": "5" }
]
}
},
"links": {
"self": "/articles/1"
}
}The attributes object holds the resource's data. It must not contain type, id, or any key that is also in relationships. Attribute names use kebab-case or camelCase — the spec recommends kebab-case but enforces consistency within a single API. Member names containing +, &, or / are forbidden to ensure safe use as URL query parameter keys.
The relationships object describes connections to other resources. Each relationship object contains at least 1 of: data (resource linkage — type/id pairs), links (self and related URLs), or meta. Resource linkage via data references the related resource without embedding it — the actual resource data lives in included when sideloaded. To validate that your resource object structure is correct, use JSON Schema to define constraints and check payloads programmatically.
Compound documents and sideloading with the included array
A compound document contains the primary data plus related resources sideloaded in the top-level included array. This eliminates N+1 HTTP requests: instead of fetching 1 article then 1 separate request per author and tag, a single compound document response delivers everything. For a list of 20 articles with authors and tags, a naive REST API requires 41+ requests; a JSON:API compound document requires exactly 1.
// GET /articles/1?include=author,tags
{
"data": {
"type": "articles",
"id": "1",
"attributes": { "title": "JSON:API Guide" },
"relationships": {
"author": { "data": { "type": "people", "id": "9" } },
"tags": { "data": [{ "type": "tags", "id": "2" }] }
}
},
"included": [
{
"type": "people",
"id": "9",
"attributes": { "name": "Alice Chen", "twitter": "@alicechen" }
},
{
"type": "tags",
"id": "2",
"attributes": { "name": "json-api", "slug": "json-api" }
}
]
}The client resolves sideloaded resources by matching the type and id of a relationship's data against entries in included. Every resource in included must be referenced by at least 1 resource in the primary data or by another resource in included — unreferenced resources are forbidden. This guarantee means clients can safely build a complete resource graph from a single response with no dangling references.
The include query parameter controls which relationships to sideload:?include=author sideloads the author, ?include=author,tags sideloads both, and ?include=author.employer traverses 2 levels. Servers MUST return a 400 Bad Request if a client requests an include path the server does not support. Use the Fetch API to construct these requests in JavaScript.
Sparse fieldsets — reduce response size by 40–70% with ?fields
Sparse fieldsets let clients request only specific attributes per resource type, cutting response size by 40–70% when resources have many fields. The query parameter format is ?fields[type]=attr1,attr2 where type matches the resource's type string. The fields parameter applies to both primary data and included resources of that type.
// Full request — returns all attributes
// GET /articles?include=author
// Sparse fieldset request — returns only title for articles, only name for people
// GET /articles?include=author&fields[articles]=title&fields[people]=name
// Response with sparse fieldsets applied
{
"data": [
{
"type": "articles",
"id": "1",
"attributes": { "title": "JSON:API Guide" },
// body, word-count, published, created-at are omitted
"relationships": {
"author": { "data": { "type": "people", "id": "9" } }
}
}
],
"included": [
{
"type": "people",
"id": "9",
"attributes": { "name": "Alice Chen" }
// twitter, bio, avatar-url are omitted
}
]
}The spec requires that relationships are never filtered by sparse fieldsets — only attributes are affected. This means clients always receive relationship linkage and can request related resources separately if needed. On the server, most JSON:API libraries handle sparse fieldsets automatically when you pass the parsed fields parameter to the serializer. In Node.js with jsonapi-serializer:
const serializer = new JSONAPISerializer('articles', {
attributes: ['title', 'body', 'word-count', 'published'],
keyForAttribute: 'kebab-case',
})
// Pass fields from query string to filter attributes
const fields = req.query.fields // { articles: 'title', people: 'name' }
const payload = serializer.serialize(articles, { fields })Sparse fieldsets combine with include for maximum efficiency: a mobile client rendering an article list might request ?include=author&fields[articles]=title,published&fields[people]=name,avatar-url, getting exactly the 4 fields it renders per article in a single HTTP round-trip. To understand how to structure these requests, see fetching JSON in JavaScript.
Query parameters — filtering, sorting, and pagination
JSON:API reserves 4 query parameter families and leaves their exact semantics to the server: filter, sort, page, and include. The spec does not mandate a specific filter syntax — common conventions include bracket notation (?filter[status]=published) and comma-separated values (?filter[tags]=json,api).
// Filtering — server-defined syntax, bracket notation is most common
GET /articles?filter[status]=published
GET /articles?filter[author.name]=Alice
GET /articles?filter[created-after]=2026-01-01
// Sorting — comma-separated field names; prefix - for descending
GET /articles?sort=created-at // ascending
GET /articles?sort=-created-at // descending
GET /articles?sort=-created-at,title // multiple: newest first, then by title
// Pagination — 2 common strategies
// Offset-based
GET /articles?page[offset]=0&page[limit]=10
// Cursor-based (more stable for large, frequently-updated datasets)
GET /articles?page[cursor]=eyJpZCI6MTAwfQ&page[size]=10The links object in collection responses provides pagination navigation without clients needing to construct URLs manually. The standard link names are self, first, prev, next, and last. A page with no previous results sets "prev": null; the last page sets "next": null. Clients should follow next rather than constructing pagination URLs to remain decoupled from the server's pagination implementation. A 400 Bad Request with a JSON:API error document is returned when a client sends an unsupported filter or sort parameter — the error's source.parameter field identifies the offending query parameter.
JSON:API error objects — status, title, detail, source.pointer
JSON:API error responses use a top-level errors array — never an errors key nested inside data. The document MUST NOT contain data when errors is present. Each error object supports 8 members, all optional individually but collectively sufficient for complete error reporting including form field mapping.
// HTTP 422 Unprocessable Entity — validation errors on article creation
{
"errors": [
{
"id": "err-001", // unique identifier for this error occurrence (for logs)
"status": "422", // HTTP status code as a STRING
"code": "VALIDATION_ERROR", // app-specific error code
"title": "Validation Error", // short, stable summary — same for all occurrences of this type
"detail": "Title must be at least 3 characters (got 1).", // occurrence-specific detail
"source": {
"pointer": "/data/attributes/title" // JSON Pointer to offending field
}
},
{
"status": "422",
"title": "Validation Error",
"detail": "Body cannot be blank.",
"source": {
"pointer": "/data/attributes/body"
}
}
]
}The source.pointer field is a JSON Pointer (RFC 6901) identifying the field that caused the error. Clients use this to highlight the correct form input without parsing the detail string. For query parameter errors, use source.parameter instead: "parameter": "filter[status]". The HTTP status code on the response envelope should be the most general code that covers all errors in the array (e.g., 422 if all are validation errors; 400 if there is a mix of bad request types). Each individual error's status member can be more specific. For general JSON patterns like error envelopes and response structures, see JSON examples.
JSON:API vs plain REST JSON vs GraphQL — comparison
The 3 most common approaches to structuring API responses — plain REST JSON, JSON:API, and GraphQL — each make different tradeoffs across 10 dimensions. The table covers every meaningful difference for teams evaluating which approach to adopt.
| Dimension | Plain REST JSON | JSON:API | GraphQL |
|---|---|---|---|
| Response structure | Ad-hoc per API | Standardized envelope (data/errors/meta) | { data: { operationName: ... } } |
| Content-Type | application/json | application/vnd.api+json | application/json |
| HTTP caching | Full (GET is cacheable) | Full (GET is cacheable) | Limited (POST by default; persisted queries needed) |
| Over-fetching | Common without custom endpoints | Solved via sparse fieldsets (?fields) | Solved via query selection sets |
| Under-fetching / N+1 | Common without custom endpoints | Solved via include (?include=) | Solved via nested query fields |
| Client library needed | No (any HTTP client) | No (any HTTP client; optional helpers) | Yes (Apollo, urql, etc. strongly recommended) |
| Type system | None (or via JSON Schema separately) | Loose (type string on resource objects) | Strongly typed (SDL schema, introspection) |
| Error structure | Ad-hoc per API | Standardized (id, status, title, detail, source) | Top-level errors array, but format varies |
| Pagination | Ad-hoc per API | Standardized links (first/prev/next/last) | Convention-based (Relay cursor, offset, etc.) |
| Best for | Simple CRUD, small teams, internal APIs | CRUD-heavy SaaS, mobile APIs, multi-client | Public APIs, graph-shaped data, diverse consumers |
Decision guidance: Choose plain REST JSON for internal APIs where teams control both client and server and consistency across endpoints is not a priority. Choose JSON:API when you have multiple client teams consuming the same API, complex related data, or want standardized error handling without adopting a query language. Choose GraphQL when your data is graph-shaped with highly variable query patterns per consumer, or when you need a strongly typed schema for tooling. For teams building mobile apps or SPAs against a CRUD backend, JSON:API typically delivers the best tradeoff — see structuring REST API JSON responses for plain REST alternatives.
Writing data — POST, PATCH, and DELETE in JSON:API
JSON:API defines write operations for 3 HTTP methods. POST creates a resource, PATCH updates an existing resource or relationship, and DELETE removes a resource. All 3 require the application/vnd.api+json Content-Type on the request.
// POST /articles — create a new article
// Request body
{
"data": {
"type": "articles", // type REQUIRED; id OPTIONAL (server assigns if omitted)
"attributes": {
"title": "JSON:API in Production",
"body": "Here is what we learned after 2 years..."
},
"relationships": {
"author": {
"data": { "type": "people", "id": "9" }
}
}
}
}
// PATCH /articles/1 — update specific fields (partial update like JSON Merge Patch)
// Only send the fields you want to change
{
"data": {
"type": "articles",
"id": "1", // id REQUIRED on PATCH
"attributes": {
"title": "JSON:API in Production (Updated)"
}
}
}
// DELETE /articles/1 — no request body required
// Success: 204 No Content (or 200 with meta if the server returns metadata)The PATCH semantics in JSON:API are partial-update by default: only the attributes included in the request body are updated, similar to HTTP PATCH semantics. Attributes absent from the request are left unchanged on the server. This differs from HTTP PUT, which replaces the entire resource. JSON:API also supports relationship update endpoints: PATCH /articles/1/relationships/tags with a body of { "data": [{"type": "tags", "id": "3"}] } replaces the article's tag relationships entirely; POST to the same URL appends tags; DELETE removes specific tags. These relationship endpoints decouple resource attribute updates from relationship updates, enabling fine-grained authorization (e.g., editors can update attributes but only admins can change relationships).
Frequently asked questions
What is JSON:API and why use it instead of plain REST JSON?
JSON:API is a specification published at jsonapi.org (current version 1.1) that defines a standard envelope for REST API responses. Instead of every API team inventing its own response structure, JSON:API mandates a top-level document with at least 1 of:data (the primary resource or array of resources), errors (an array of error objects), or meta (non-standard information). A document MUST NOT contain both data and errorssimultaneously. Each resource object must have a type and id, with attributes in an attributes object and related resources referenced via a relationships object rather than embedded inline. The advantages over plain REST JSON are concrete: clients do not need per-API parsing logic because the structure is predictable; the included array lets servers sideload related resources in a single response, reducing N+1 HTTP requests by 40–60%; sparse fieldsets cut response size by 40–70%; and standardized error objects with status, title, and detail give clients consistent error handling across every endpoint. For foundational JSON knowledge, see JSON examples.
What is the required structure of a JSON:API resource object?
A JSON:API resource object must have exactly 2 required members: type (a string identifying the resource type, e.g. "articles") and id (a string identifying the specific resource — numeric database IDs must be stringified, e.g. "1" not 1). Beyond those 2 required members, a resource object may include: attributes (an object containing the resource's data fields — the object itself cannot contain type, id, or relationships), relationships (an object whose keys are relationship names, each value being a relationship object with a data member containing a resource linkage), links (self and related URLs), and meta(non-standard metadata). The strict separation of attributes from relationships is intentional — it makes it unambiguous whether a field is a data attribute or a link to another resource, enabling generic client libraries to traverse the document graph without any schema knowledge. Validate your resource object structure with JSON Schema.
How do relationships work in JSON:API (compound documents)?
In JSON:API, relationships describe connections between resources without embedding the full related object inline. A relationship object contains a data member (resource linkage) with type and id of the related resource. For a to-one relationship: "author": {"data": {"type": "people", "id": "9"}}. For a to-many relationship, data is an array of type/id objects. When the client needs the full related resource data in the same response, the server sideloads it in the top-level included array — this is called a compound document. To request sideloading, clients pass ?include=author,tags. This eliminates separate HTTP requests for each related resource — for a 20-article list page with author data, it reduces 21 HTTP requests down to 1. The include parameter can traverse multiple levels: ?include=author.employer includes the author's employer resource as well. See fetching JSON in JavaScript to build these requests client-side.
How do I implement sparse fieldsets in JSON:API?
Sparse fieldsets let clients request only specific attributes, reducing response size by 40–70%. The query parameter format is ?fields[type]=attr1,attr2 where type matches the resource type string. For example, GET /articles?fields[articles]=title,body returns only the title and body attributes for articles, omitting all others. Multiple types can be specified in one call: GET /articles?include=author&fields[articles]=title&fields[people]=name returns articles with only title and authors with only name. Sparse fieldsets apply to both primary data and included resources of that type. On the server, most JSON:API libraries (jsonapi-serializer for Node.js, fast_jsonapi for Ruby, marshmallow-jsonapi for Python) handle sparse fieldsets automatically when you pass the parsed fields parameter from the request to the serializer. The spec requires that relationships are never filtered — only attributes are affected. This combination of include and sparse fields is the most impactful optimization in JSON:API for bandwidth-constrained mobile clients.
How does JSON:API compare to GraphQL for API design?
JSON:API and GraphQL both solve over-fetching and under-fetching problems of plain REST, but with different architectural tradeoffs. JSON:API uses standard HTTP GET/POST/PATCH/DELETE with a fixed document envelope — it is cacheable at the HTTP layer (CDNs, browser cache, and reverse proxies work with zero configuration). GraphQL uses a single POST endpoint with a query language — HTTP caching requires additional layers like persisted queries. JSON:API sparse fieldsets and include parameters cover most query flexibility for CRUD-heavy APIs. GraphQL is more expressive for deeply nested, graph-shaped data with highly variable query shapes per client. JSON:API has lower client overhead — any HTTP client works. GraphQL requires a client library and schema introspection. For teams building admin panels, mobile APIs, or CRUD-heavy SaaS products, JSON:API delivers 80% of GraphQL's flexibility with a fraction of the tooling complexity. GraphQL wins for public APIs with many diverse consumers querying the same data in very different shapes. See the comparison table above for a full breakdown across 10 dimensions.
What does the JSON:API error object look like?
A JSON:API error document has a top-level errors array — it must NOT contain a data member simultaneously. Each error object in the array can contain: id (a unique identifier for this error occurrence, useful for log correlation), status (the HTTP status code as a string, e.g. "422"), code (an application-specific error code string), title (a short, stable summary that should not change between occurrences), detail (a human-readable explanation specific to this occurrence), source (an object with pointer — a JSON Pointer to the field that caused the error, e.g. "/data/attributes/title" — or parameter, the query parameter that caused the error), and links.about (a URL with more information about the error). The source.pointer field is especially useful for form validation: clients can map each error directly to the form field that caused it without parsing the detail string. Multiple errors from a single request (e.g., 3 validation failures) are returned in the same errors array in a single response — clients never need to re-submit to discover subsequent errors.
Definitions
- Resource object
- The atomic unit of JSON:API data. Must contain
typeandidstrings; optionally containsattributes,relationships,links, andmeta. - Compound document
- A JSON:API response that contains both primary
dataand related resources sideloaded in the top-levelincludedarray, enabling a single HTTP request to deliver a complete resource graph. - Sparse fieldsets
- A query mechanism (
?fields[type]=attr1,attr2) that restricts which attributes the server returns for a given resource type, reducing response size by 40–70%. - Relationships
- Named connections from one resource to others, expressed as type/id resource linkages in the
relationshipsobject of a resource object — without embedding the related resource's attributes inline. - Sideloading
- Including full related resource objects in the top-level
includedarray of a response so clients receive all needed data in a single request rather than making N+1 requests. - Content negotiation
- The process by which client and server agree on the media type of a request or response. JSON:API requires both parties to use
application/vnd.api+jsonas the Content-Type and Accept header value.
Ready to validate your JSON:API payloads?
Use Jsonic's JSON Formatter to prettify and validate any JSON:API request or response body. You can also use the JSON Diff tool to compare two API responses and spot structural differences instantly.
Open JSON Formatter