I'm planning a series of pull requests for JSON API, many of which are related to issues already raised on Github. I've summarized them here to emphasize common themes:
- Center requests and responses upon primary resources and reference other resources relatively.
- By default, make as few assumptions as possible about what the client wants.
- Allow for a number of degrees of flexibility, all of which are optional, so that the client can customize the response.
- Provide sufficient recommendations for common API design patterns to inform default implementations.
I touched on a lot of these topics in my talk "Building Ambitious APIs with Ruby" at the Burlington Ruby Conference this summer: video + slides
Instead of including primary and related resources together at the same level, separate the related resources in a nested object.
It's important to separate the primary resource from related resources because:
- It allows the client to easily identify the primary resource among related resources of the same type. This is especially important when creating a resource without prior knowledge of its server-assigned id.
- It clarifies issues like pagination that are specific to the primary resource.
This can be achieved by nesting related resources in a linked
object. For instance:
{
"posts": [{
"id": "1",
"name": "Rails is Omakase",
"links": {
"author": "9",
"related": ["2"]
}
}],
"linked": {
"posts": [{
"id": "2",
"name": "The Parley Letter",
"links": {
"author": "9",
"related": ["1"]
}
}],
"people": [{
"id": "9",
"name": "DHH"
}]
}
}
It's easy to imagine a scenario in which resources that are members of the primary array are secondary resources for other members of the primary array (e.g. people + friends). In those scenarios, primary resources should not be duplicated as secondary resources in linked
. In other words, a single canonical representation of a resource should continue to be returned with each response.
If this change is adopted, I think we should consider returning either a singular or plural primary resource as appropriate for the request, instead of always returning an array. For instance, a request to create a resource would return just that resource as an object. I believe this is the path of least surprise and is semantically correct. Nesting related resources in linked
would remove one of the primary motivations to always return an array.
Clarify that compound document support is optional and that assumptions should be avoided when composing a compound document. Require explicit requests for the inclusion of related resources.
By default, no related resources will be included when a resource is requested.
Inclusion of related resources will be supported with an include
parameter. For instance:
/posts/1?include=comments
In order to include resources related to other resources, the dot-separated path of each resource should be specified:
/posts/1?include=comments.authors
Note that a request for comments.authors
will not automatically also include comments
in the response (although comments will obviously need to be queried in order to fulfill the request for comment authors).
Multiple related resources can be requested in a comma-separated list:
/posts/1?include=authors,comments,comments.authors
Provide recommendations for requesting custom per-resource fieldsets. These recommendations are optional but intended to inform default implementations.
There are strong client and server side performance benefits to providing a convention by which a client can request a subset of fields per resource type. Some fields are either large (like associated binary data) or expensive to calculate. In fact, any unneeded fields become expensive when multiplied over a large enough response.
Resources should return all fields by default, but may provide the option to request only specific fields with query parameters. The primary resource type's fields may be specified with the fields
parameter. For example:
/people?fields=id,name,age
The fields for any resource types (including the primary resource type) can be specified by including a [resource_type]_fields
parameter. For instance:
/posts?include=comments,authors&post_fields=id,title&person_fields=id,name&comment_fields=id,body
Note that post_fields
and fields
would be interchangeable in the previous example.
Provide recommendations for filtering. These recommendations are optional but intended to inform default implementations.
Filters should apply to the primary resource.
Each filter parameter may supply one or more comma-separated values. If any value matches, then the filter condition will be considered met. For example, the following request will return posts with a status of draft
OR published
:
/posts?status=draft,published
Multiple filter parameters may be chained together. The following request will return posts with a status of draft
AND created by the author with an id of 1
:
/posts?status=draft&author=1
Perhaps include recommendations for comparison operators other than equality: gt, lt, gte, lte, etc.
Provide recommendations for sorting. These recommendations are optional but intended to inform default implementations.
Sorting should apply to the primary resource.
The sort
parameter should be used to specify one or more fields by which results should be sorted:
/people?sort=name,age
The default sort order should be ascending. A descending sort order can be indicated with a -
prefix:
/people?sort=-name
Provide recommendations for paginating resources. These recommendations are optional but intended to inform default implementations.
Standardize upon pagination parameters, such as page
and per_page
, to be used to request pages of data:
/posts?page=1&per_page=100
Although RFC5988 covers web linking and recommends using the link header field, JSON API currently recommends using the top level meta
object to specify links for pagination.
So what is the best way to include rel
links in meta
instead of the LINK header?
A rel
object could be embedded in meta
. This object could contain first
, last
, next
and prev
pointers as appropriate. Of course, these links should contain any filtering and sorting parameters required to page over the current result set.
It could also be useful to allow an optional total
field in meta
.
{
"meta": {
"total": "820",
"rel": {
"next": "/posts?author=9&page=2&per_page=100",
"last": "/posts?author=9&page=9&per_page=100"
}
},
"posts": [{
"id": "1",
"name": "Rails is Omakase",
"links": {
"author": "9",
"related": ["2"]
},
{
"id": "2",
"name": "The Parley Letter",
"links": {
"author": "9",
"related": ["1"]
}
}
},
…
]
}
I'd like to discuss pagination of related resources in a compound document. The top-level links
object contains an href
for each link
. Maybe it's sufficient to say that any related resource should follow that link if the resource count exceeds the allowed limit? If that's true, what should indicate that paging is needed? It seems that rel
links and total
counts could be useful for related resources, and therefore, both could be duplicated for related resources.
One option would be to simply repeat the meta
structure for related resources. For instance:
{
"meta": {
"total": "820",
"rel": {
"next": "/posts?author=9&page=2&per_page=100",
"last": "/posts?author=9&page=9&per_page=100"
},
"linked": {
"posts.author": {
"total": "1"
},
"posts.comments": {
"total": "1050",
"rel": {
"next": "/comments?author=9&page=2&per_page=100",
"last": "/comments?author=9&page=11&per_page=100"
},
}
}
},
"posts": [
…
],
"linked": {
"people": [
...
],
"comments": [
...
]
}
}
I'm not entirely happy with how these rel
links are in meta
, while the spec also recommends specifying links in the sibling links
object.
Maybe it would be better to include all links in links
instead of meta
?
One other related concern is that an array of ids (e.g. for tweets) for related resources may itself become too large to include in a response. So far, we've just discussed pagination of resources, not ids.
Needless to say, this is a non-trivial problem and needs further discussion. I'm not even sure that any recommendations should be made for pagination of related resources, but then again, it's an obvious sticking point for compound documents. Let's talk soon...