Getting started
Introduction
JSON:API is a specification for how a client should request resources and how a server should respond to those requests. It's designed to minimize both the number of requests and the amount of data transmitted between clients and servers.
Version: v1.1 (September 30, 2022)
Official Spec: jsonapi.org
Media Type
The JSON:API media type is:
application/vnd.api+json
Critical: Content-Type and Accept headers MUST use this exact media type without any parameters.
Basic Response
{
"data": {
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON:API paints my bikeshed!"
}
}
}
Every JSON:API document MUST contain at least one of: data, errors, or meta at the top level.
Document Structure
Top-Level Members
| Member | Required | Description |
|---|---|---|
data |
Conditional | Primary data |
errors |
Conditional | Array of error objects |
meta |
Optional | Non-standard meta-information |
jsonapi |
Optional | JSON:API version info |
links |
Optional | Links related to primary data |
included |
Optional | Related resources |
Top-Level Rules
{
"data": { ... }, // Primary data
"included": [ ... ], // Side-loaded resources
"meta": { ... }, // Document-level metadata
"links": { ... }, // Pagination, self links
"jsonapi": { // API version info
"version": "1.1"
}
}
Critical Rules:
dataanderrorsMUST NOT coexistincludedrequiresdatato be present- At least one of
data,errors, ormetaMUST be present
Resource Collections
{
"data": [
{
"type": "articles",
"id": "1",
"attributes": { ... }
},
{
"type": "articles",
"id": "2",
"attributes": { ... }
}
]
}
Collections are arrays of resource objects.
Resource Objects
Required Members
{
"type": "articles", // Required
"id": "1" // Required (except on POST)
}
Every resource object MUST contain type and id members (except when creating via POST).
Full Resource Structure
{
"type": "articles",
"id": "1",
"attributes": {
"title": "Rails is Omakase",
"created": "2023-01-15"
},
"relationships": {
"author": {
"links": {
"self": "/articles/1/relationships/author",
"related": "/articles/1/author"
},
"data": { "type": "people", "id": "9" }
}
},
"links": {
"self": "/articles/1"
},
"meta": {
"views": 1024
}
}
Resource Members
| Member | Description |
|---|---|
type |
Resource type (string) |
id |
Resource identifier (string) |
attributes |
Attributes object |
relationships |
Relationships object |
links |
Links object |
meta |
Non-standard metadata |
Field Naming
Valid: "first-name", "firstName", "first_name"
Invalid: Contains special characters except - and _
- MUST use same name for same attribute across types
- MUST use same case style consistently
- SHOULD use hyphen-case (recommended)
Relationships
Relationship Object
{
"relationships": {
"author": {
"links": {
"self": "/articles/1/relationships/author",
"related": "/articles/1/author"
},
"data": { "type": "people", "id": "9" },
"meta": {
"verified": true
}
}
}
}
A relationship object MUST contain at least one of: links, data, or meta.
Resource Linkage
To-One Relationship:
{
"data": { "type": "people", "id": "9" }
}
To-Many Relationship:
{
"data": [
{ "type": "comments", "id": "5" },
{ "type": "comments", "id": "12" }
]
}
Empty Relationship:
{
"data": null // to-one
}
{
"data": [] // to-many
}
Relationship Links
{
"links": {
"self": "/articles/1/relationships/comments",
"related": "/articles/1/comments"
}
}
| Link | Purpose |
|---|---|
self |
Relationship itself |
related |
Related resource(s) |
Query Parameters
Include (Compound Documents)
GET /articles/1?include=author
GET /articles/1?include=author,comments
GET /articles/1?include=author,comments.author
Comma-separated relationship paths. Dot-notation for nested relationships.
Response:
{
"data": {
"type": "articles",
"id": "1",
"relationships": {
"author": {
"data": { "type": "people", "id": "9" }
}
}
},
"included": [
{
"type": "people",
"id": "9",
"attributes": {
"name": "Dan Gebhardt"
}
}
]
}
Sparse Fieldsets
GET /articles?fields[articles]=title,body
GET /articles?fields[articles]=title&fields[people]=name
Request only specific fields. Format: fields[TYPE]=field1,field2
Response:
{
"data": [
{
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON:API paints my bikeshed!"
}
}
]
}
Sorting
GET /articles?sort=created
GET /articles?sort=-created
GET /articles?sort=-created,title
Comma-separated sort fields. Prefix with - for descending order.
Pagination
GET /articles?page[number]=3&page[size]=20
GET /articles?page[offset]=40&page[limit]=20
Note: Pagination strategy is implementation-specific.
Response with links:
{
"links": {
"self": "/articles?page[number]=3",
"first": "/articles?page[number]=1",
"prev": "/articles?page[number]=2",
"next": "/articles?page[number]=4",
"last": "/articles?page[number]=13"
},
"data": [ ... ]
}
Filtering
GET /articles?filter[published]=true
GET /comments?filter[post]=1,2
Note: Filtering strategy is implementation-specific.
CRUD Operations
Fetch Collection (GET)
Request:
GET /articles HTTP/1.1
Accept: application/vnd.api+json
Response (200 OK):
{
"data": [
{
"type": "articles",
"id": "1",
"attributes": { ... }
}
]
}
Fetch Single Resource (GET)
Request:
GET /articles/1 HTTP/1.1
Accept: application/vnd.api+json
Response (200 OK):
{
"data": {
"type": "articles",
"id": "1",
"attributes": { ... }
}
}
Not Found (404):
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Article not found"
}
]
}
Create Resource (POST)
Request:
POST /articles HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "articles",
"attributes": {
"title": "New Article",
"body": "Content here"
},
"relationships": {
"author": {
"data": { "type": "people", "id": "9" }
}
}
}
}
Response (201 Created):
HTTP/1.1 201 Created
Location: /articles/13
Content-Type: application/vnd.api+json
{
"data": {
"type": "articles",
"id": "13",
"attributes": {
"title": "New Article",
"body": "Content here"
}
}
}
Client-Generated IDs:
{
"data": {
"type": "articles",
"id": "uuid-generated-by-client",
"attributes": { ... }
}
}
Update Resource (PATCH)
Request:
PATCH /articles/1 HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "articles",
"id": "1",
"attributes": {
"title": "Updated Title"
}
}
}
Response (200 OK) - with body:
{
"data": {
"type": "articles",
"id": "1",
"attributes": {
"title": "Updated Title"
}
}
}
Response (204 No Content) - without body:
HTTP/1.1 204 No Content
Update Relationships (PATCH)
Request:
PATCH /articles/1/relationships/author HTTP/1.1
Content-Type: application/vnd.api+json
{
"data": { "type": "people", "id": "12" }
}
Delete Resource (DELETE)
Request:
DELETE /articles/1 HTTP/1.1
Accept: application/vnd.api+json
Response (204 No Content):
HTTP/1.1 204 No Content
Response (200 OK) - with meta:
{
"meta": {
"deleted_at": "2023-01-15T10:30:00Z"
}
}
Error Objects
Error Structure
{
"errors": [
{
"id": "unique-error-id",
"links": {
"about": "/docs/errors/validation"
},
"status": "422",
"code": "VALIDATION_ERROR",
"title": "Validation Failed",
"detail": "Title must not be blank",
"source": {
"pointer": "/data/attributes/title"
},
"meta": {
"timestamp": "2023-01-15T10:30:00Z"
}
}
]
}
All error object members are optional.
Error Members
| Member | Description |
|---|---|
id |
Unique error identifier |
links.about |
Link to error details |
status |
HTTP status code (string) |
code |
Application-specific code |
title |
Short human-readable summary |
detail |
Specific error explanation |
source |
Error source reference |
meta |
Non-standard metadata |
Error Source
Pointer (JSON Pointer to attribute):
{
"source": {
"pointer": "/data/attributes/email"
}
}
Parameter (query parameter):
{
"source": {
"parameter": "include"
}
}
Header (HTTP header):
{
"source": {
"header": "Content-Type"
}
}
Common Error Responses
400 Bad Request:
{
"errors": [
{
"status": "400",
"title": "Bad Request",
"detail": "Invalid JSON syntax"
}
]
}
403 Forbidden:
{
"errors": [
{
"status": "403",
"title": "Forbidden",
"detail": "You don't have permission to delete this resource"
}
]
}
422 Unprocessable Entity:
{
"errors": [
{
"status": "422",
"source": { "pointer": "/data/attributes/email" },
"title": "Invalid Attribute",
"detail": "Email must be a valid email address"
}
]
}
Links
Link Object Forms
String URL:
{
"links": {
"self": "/articles/1"
}
}
Link Object:
{
"links": {
"related": {
"href": "/articles/1/author",
"meta": {
"count": 1
}
}
}
}
Common Link Types
| Link | Context | Description |
|---|---|---|
self |
Top-level | Current request URL |
related |
Relationship | Related resource URL |
first |
Pagination | First page |
last |
Pagination | Last page |
prev |
Pagination | Previous page |
next |
Pagination | Next page |
Pagination Links
{
"links": {
"self": "/articles?page[number]=3",
"first": "/articles?page[number]=1",
"prev": "/articles?page[number]=2",
"next": "/articles?page[number]=4",
"last": "/articles?page[number]=10"
},
"data": [ ... ]
}
Meta Information
Document-Level Meta
{
"meta": {
"total": 120,
"page": {
"current": 3,
"total": 12
},
"copyright": "Copyright 2023 Example Corp."
},
"data": [ ... ]
}
Meta can appear at document, resource, relationship, or link level.
Resource-Level Meta
{
"data": {
"type": "articles",
"id": "1",
"attributes": { ... },
"meta": {
"views": 1024,
"created_by": "user-123"
}
}
}
Relationship Meta
{
"relationships": {
"comments": {
"data": [ ... ],
"meta": {
"total": 42
}
}
}
}
JSON:API Object
Version Information
{
"jsonapi": {
"version": "1.1"
},
"data": { ... }
}
Optional member indicating JSON:API version.
With Meta
{
"jsonapi": {
"version": "1.1",
"meta": {
"server": "my-api-v2"
}
},
"data": { ... }
}
Advanced Patterns
Compound Documents
Request:
GET /articles/1?include=author,comments,comments.author
Response:
{
"data": {
"type": "articles",
"id": "1",
"attributes": { "title": "JSON:API" },
"relationships": {
"author": {
"data": { "type": "people", "id": "9" }
},
"comments": {
"data": [
{ "type": "comments", "id": "5" },
{ "type": "comments", "id": "12" }
]
}
}
},
"included": [
{
"type": "people",
"id": "9",
"attributes": { "name": "Dan" }
},
{
"type": "comments",
"id": "5",
"attributes": { "body": "Great!" },
"relationships": {
"author": {
"data": { "type": "people", "id": "10" }
}
}
},
{
"type": "people",
"id": "10",
"attributes": { "name": "Jane" }
}
]
}
Sparse Fieldsets with Relationships
GET /articles?include=author&fields[articles]=title,author&fields[people]=name
Combine includes with field selection for optimal payloads.
Bulk Operations
Create Multiple Resources (extension):
{
"data": [
{
"type": "articles",
"attributes": { "title": "First" }
},
{
"type": "articles",
"attributes": { "title": "Second" }
}
]
}
Note: Bulk operations are not in core spec but commonly implemented.
HTTP Headers
Required Headers
Requests:
Accept: application/vnd.api+json
Content-Type: application/vnd.api+json
Responses:
Content-Type: application/vnd.api+json
Status Codes
| Code | Usage |
|---|---|
200 |
Success with response body |
201 |
Resource created |
204 |
Success without body |
400 |
Bad request syntax |
403 |
Forbidden |
404 |
Resource not found |
409 |
Conflict (duplicate ID) |
415 |
Unsupported media type |
422 |
Validation error |
500 |
Server error |
Content Negotiation
Server MUST:
- Return
415if Content-Type has media type parameters - Return
406if Accept header doesn't include JSON:API media type
Example rejection:
Content-Type: application/vnd.api+json; charset=utf-8
Response: 415 Unsupported Media Type
Common Patterns
Filtering by Related Resource
GET /articles?filter[author]=9
GET /comments?filter[article.author]=9
Complex Sorting
GET /people?sort=age,-name
Sort by age ascending, then name descending.
Cursor-Based Pagination
{
"links": {
"self": "/articles?page[cursor]=abc123",
"next": "/articles?page[cursor]=def456",
"prev": "/articles?page[cursor]=xyz789"
}
}
Versioning
URL versioning:
GET /v1/articles
Header versioning:
Accept: application/vnd.api+json; version=1
Note: Spec doesn't mandate versioning strategy.
Extensions
Atomic Operations
Extension for batch operations with rollback support.
{
"atomic:operations": [
{
"op": "add",
"data": {
"type": "articles",
"attributes": { "title": "New" }
}
},
{
"op": "update",
"data": {
"type": "articles",
"id": "1",
"attributes": { "title": "Updated" }
}
}
]
}
Media type: application/vnd.api+json; ext="https://jsonapi.org/ext/atomic"
JSON:API Profiles
Extensions that add functionality while remaining compliant.
Header:
Content-Type: application/vnd.api+json; profile="http://example.com/profile"
Gotchas
Data and Errors Are Mutually Exclusive
// ❌ INVALID - both data and errors
{
"data": { ... },
"errors": [ ... ]
}
// ✅ VALID - only one
{
"data": { ... }
}
// ✅ VALID - only errors
{
"errors": [ ... ]
}
Included Without Data
// ❌ INVALID - included requires data
{
"included": [ ... ]
}
// ✅ VALID
{
"data": { ... },
"included": [ ... ]
}
Media Type Must Be Exact
// ❌ Server MUST reject with 415
Content-Type: application/vnd.api+json; charset=utf-8
// ✅ Correct
Content-Type: application/vnd.api+json
Resource Identifiers Are Strings
// ❌ INVALID - id is number
{
"type": "articles",
"id": 1
}
// ✅ VALID - id is string
{
"type": "articles",
"id": "1"
}
Relationship Data Can Be Null
// ✅ VALID - no related resource
{
"relationships": {
"author": {
"data": null
}
}
}
// ✅ VALID - missing relationship data
{
"relationships": {
"author": {
"links": {
"related": "/articles/1/author"
}
}
}
}
Type and ID Required on PATCH
// ❌ INVALID - missing type/id
PATCH /articles/1
{
"data": {
"attributes": { "title": "New" }
}
}
// ✅ VALID
PATCH /articles/1
{
"data": {
"type": "articles",
"id": "1",
"attributes": { "title": "New" }
}
}
Sparse Fieldsets Syntax
// ❌ INVALID
?fields=title,body
// ✅ VALID - must specify type
?fields[articles]=title,body
Examples
Complete GET Response with Pagination
GET /articles?page[number]=2&page[size]=10&include=author&sort=-created HTTP/1.1
Accept: application/vnd.api+json
{
"jsonapi": {
"version": "1.1"
},
"links": {
"self": "/articles?page[number]=2&page[size]=10",
"first": "/articles?page[number]=1&page[size]=10",
"prev": "/articles?page[number]=1&page[size]=10",
"next": "/articles?page[number]=3&page[size]=10",
"last": "/articles?page[number]=5&page[size]=10"
},
"data": [
{
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON:API paints my bikeshed!",
"body": "The shortest article. Ever.",
"created": "2023-05-22T14:56:29.000Z"
},
"relationships": {
"author": {
"links": {
"self": "/articles/1/relationships/author",
"related": "/articles/1/author"
},
"data": { "type": "people", "id": "9" }
}
},
"links": {
"self": "/articles/1"
},
"meta": {
"views": 1024
}
}
],
"included": [
{
"type": "people",
"id": "9",
"attributes": {
"name": "Dan Gebhardt",
"twitter": "dgeb"
},
"links": {
"self": "/people/9"
}
}
],
"meta": {
"total": 42
}
}
Complete POST Request
POST /articles HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "articles",
"attributes": {
"title": "Getting Started with JSON:API",
"body": "JSON:API is a powerful specification...",
"tags": ["json", "api", "rest"]
},
"relationships": {
"author": {
"data": { "type": "people", "id": "9" }
},
"categories": {
"data": [
{ "type": "categories", "id": "1" },
{ "type": "categories", "id": "3" }
]
}
}
}
}
Validation Error Response
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/vnd.api+json
{
"errors": [
{
"status": "422",
"source": { "pointer": "/data/attributes/title" },
"title": "Invalid Attribute",
"detail": "Title must not be blank"
},
{
"status": "422",
"source": { "pointer": "/data/attributes/body" },
"title": "Invalid Attribute",
"detail": "Body must be at least 10 characters"
}
]
}
Also see
- JSON:API Specification - Official v1.1 specification
- JSON:API Examples - Official examples
- JSON:API Implementations - Client and server libraries
- JSON Pointer RFC 6901 - Used in error source pointers
- REST API Best Practices - General REST API design patterns