NexusCS

JSON:API

API
Quick reference for JSON:API v1.1 specification - a standardized format for building APIs in JSON
api
rest
json
specification

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:

  • data and errors MUST NOT coexist
  • included requires data to be present
  • At least one of data, errors, or meta MUST 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 415 if Content-Type has media type parameters
  • Return 406 if 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