Introduction to the JSON API
Published on by Björn Brala
JSON API was originally drafted in May 2013 by Yehuda Katz and reached stable in May 2015, and it is about making your API calls efficient. You can fetch data as you need, adding or removing attributes or relations as your requirements change. This minimizes the amount of data and round trips needed when making API calls.
{json:api}
JSON API documents work as any other API format, you send a request to an endpoint and receive your document. The JSON API specification defines how resources are structured. This structure helps in normalizing how you consume the API.
For example when you make a call to the resource articles
through a simple GET
request.
GET /articles HTTP/1.1Accept: application/vnd.api+json
You receive a response which should look something like this:
// ...{ "type": "articles", "id": "1", "attributes": { "title": "Rails is Omakase" }, "relationships": { "author": { "links": { "self": "http://example.com/articles/1/relationships/author", "related": "http://example.com/articles/1/author" }, "data": { "type": "people", "id": "9" } } }, "links": { "self": "http://example.com/articles/1" }} // ...
Seems simple enough. The author relationship includes a link to the relationship itself and some basic information about the relation. Using the links in the document you can retrieve information on the related resources.
But this is only scratching the surface. There are a quite a few features which make JSON API so lovely to use.
Compound documents
To reduce the number of HTTP requests, servers MAY allow responses that include related resources along with the requested primary resources. Such responses are called “compound documents”.
A compound document is a resource which includes the data of included relations. For example, when requesting an article as shown earlier it may include the data of the author so you don’t need a second call to fetch the author of the article.
This is awesome for a few reasons. Getting the full data of a resource you want to consume in one go is unheard of unless specific endpoints are programmed. On the server side of things, caching and invalidating a request like this is easy.
Just have a look at this example which returns a set of 1 document.
{ "data": [{ "type": "articles", "id": "1", "attributes": { "title": "JSON API paints my bikeshed!" }, "links": { "self": "http://example.com/articles/1" }, "relationships": { "author": { "links": { "self": "http://example.com/articles/1/relationships/author", "related": "http://example.com/articles/1/author" }, "data": { "type": "people", "id": "9" } }, "comments": { "links": { "self": "http://example.com/articles/1/relationships/comments", "related": "http://example.com/articles/1/comments" }, "data": [{ "type": "comments", "id": "5" }, { "type": "comments", "id": "12" }] } } }], "included": [{ "type": "people", "id": "9", "attributes": { "first-name": "Dan", "last-name": "Gebhardt", "twitter": "dgeb" }, "links": { "self": "http://example.com/people/9" } }, { "type": "comments", "id": "5", "attributes": { "body": "First!" }, "relationships": { "author": { "data": { "type": "people", "id": "2" } } }, "links": { "self": "http://example.com/comments/5" } }, { "type": "comments", "id": "12", "attributes": { "body": "I like XML better" }, "relationships": { "author": { "data": { "type": "people", "id": "9" } } }, "links": { "self": "http://example.com/comments/12" } }]}
If you look at the included
property you see all the related resources for the article. Every included document has a type
property which defines which type of resource is returned and a link to the endpoint for the document.
It might feel weird to refer to the author
relation in the article
only by id
and type
and loading the related people
resource from the included
tag. But imagine when there are multiple articles referring to the same author
, the document only includes the referred people
once in the included
data. Pretty efficient!
Inclusion of related resources
In the above example, the server includes all relations in the response, but you might not want this by default since this bloats the responses a bit. For this reason, the specification provides a way to specify what relations should be included in the response.
For example, if you only need the author
relationship included you can just call the endpoint with the include
request parameter. This will tell the server to only send the specified relation with the response.
GET /articles/1?include=author HTTP/1.1Accept: application/vnd.api+json
If you need multiple relationships you separate the relations with a comma (,
).
When defining the includes you can go even further and include nested relations. For example, you want comments
, with the authors
of the comments
you can just include comments.author
:
GET /articles/1?include=author,comments.author HTTP/1.1Accept: application/vnd.api+json
This flexibility makes fetching the correct resources a breeze allowing you to tweak the results as needed.
Sparse fieldsets
When you are using compound documents your request could get large, fast. Especially when the included relationships contain a lot of data. Most of the time you do not need every single attribute defined in the resources but only want things like author names. JSON API provides sparse fieldsets for this use case.
You can specify the fields that are fetched by setting the fields
request parameter. The format is fields[TYPE]
, so you can specify per resource type what fields you need.
GET /articles?include=author&fields[articles]=title,body&fields[people]=name HTTP/1.1Accept: application/vnd.api+json
This call could include the title
and body
field from the articles
resource and the name
field from the people
resource.
Other features
Servers may implement a few more features which are defined by the specification. These features are includes, sorting, pagination and filtering.
Sorting
If the server supports it, you can sort your records using the sort
request parameter. Sorting is in ascending order by default, to sort descending you can prepend -
to the field.
GET /articles?sort=-created,title HTTP/1.1Accept: application/vnd.api+json
Pagination
If the server supports pagination it will supply a few extra meta attributes with pagination information. The server can decide how to paginate themselves, be it through an offset, or direct page numbers. An example of an implementation uses the required links to other pages and includes some metadata in the meta tag.
{ links: { first: "http://example.com/articles?page=1", last: "http://example.com/articles?page=262", prev: "http://example.com/articles?page=261", next: null }, meta: { current_page: 262, from: 3916, last_page: 262, per_page: 15, to: 3924, total: 3924 }}
Filtering
The spec says little about filtering, it just states there may be a parameter which is called filter
which is used to fulfil filtering on the server. The implementation is specific for the server and can be anything.
Creating, Updating and Deleting resources
When you understand the structure of documents, creating and updating resources is a breeze. It uses the standard HTTP verbs to communicate the desired action of the request. GET
for fetching, POST
for creating, PATCH
for (partial) updating and DELETE
for deleting resources.
Creating resources
POST /photos HTTP/1.1Content-Type: application/vnd.api+jsonAccept: application/vnd.api+json { "data": { "type": "photos", "attributes": { "title": "Ember Hamster", "src": "http://example.com/images/productivity.png" }, "relationships": { "photographer": { "data": { "type": "people", "id": "9" } } } }}
This is an example of posting a new photo. As you notice the relationship is included in the request in the relationships
property.
Updating resources
PATCH /articles/1 HTTP/1.1Content-Type: application/vnd.api+jsonAccept: application/vnd.api+json { "data": { "type": "articles", "id": "1", "attributes": { "title": "To TDD or Not" } }}
When updating a resource it will update the posted fields to the new values. Values that are not included in the request will not be updated.
Updating relationships
It is possible to update relationships in 2 ways. One way is to include the relation in the PATCH
request. The same way as you have seen earlier. Another way is using the specific relationship endpoint.
PATCH /articles/1/relationships/author HTTP/1.1Content-Type: application/vnd.api+jsonAccept: application/vnd.api+json { "data": { "type": "people", "id": "12" }}
This call will update the to-one relationship on the article
. If you want to delete the relation, you can pass null
as the data.
If you want to update a to-many relation you can just send an array of relations to the endpoint. This will replace all members in the relation with the data you post.
PATCH /articles/1/relationships/tags HTTP/1.1Content-Type: application/vnd.api+jsonAccept: application/vnd.api+json { "data": [ { "type": "tags", "id": "2" }, { "type": "tags", "id": "3" } ]}
When clearing a to-many relation, just send an empty array as data.
Deleting resources
There isn’t much to say about deleting resources, you just send a DELETE
call to the endpoint.
DELETE /photos/1 HTTP/1.1Accept: application/vnd.api+json
Learn more
If you want to learn more about the details of {json:api} and its usage, check out jsonapi.org. Their specification is quite clear, although there are a lot of features that are not mandatory for servers to implement. On their site, they also maintain a list of client and server implementations for you to get up and running quickly.
They are currently working on version 1.1 of the specification. It will be 100% backwards compatible with version 1.0, only adding new features, breaking nothing.
{json:api} <3
Hopefully, you’ve learned what makes this specification awesome. Personally, I love the structure, consistency and flexibility, this makes communicating with an API a lot easier. You always know what to expect, and don’t need loads of requests to get your data.
For this reason we have been working on a Laravel package for mapping remote {json:api} resources to Eloquent like models and collections, and a package to generate an {json:api} from your Eloquent models.
Technical director at SWIS, on a quest to make the internet a better place through open source.