Description
This proposal is a successor to #590. I would like to request to consider this for 1.0 because it won't make much sense afterwards.
@ethanresnick has submitted #618 while I was thinking about this. Even though the content of #618 is a part of what is proposed here, the scope of this issue is different from #618, so I am not hijacking #618...
Motivation
- Five HTTP verbs are not enough to describe all possible actions available for a resource in a complex API. The solution is to add custom endpoints. There should be a way to associate those custom endpoints with the primary endpoint of the resource.
- It would be great if JSON API allowed to build HATEOAS-compatible APIs. This requires ability to dynamically determine which actions can be taken on a given resource and its relationships, as well as what are the starting points to explore the API without much prior knowledge about it. It is often important for the clients to save trips to the server, so such information should be returned as part of a response, not on a separate endpoint.
- Bring consistency to top-level and resource-level
links
and to relationships. A lot has been said on this. One thing I am trying to solve here is that each relationship provides multiple links, whereas members of top-levellinks
and resource-levelself
are singular links, so they are not similar at all, one is a actually a component of another. Another aspect of it is making alllinks
have the same structure, which can also be considered as a step towards the concept of "links
can appear anywhere" (reserve attributes in top level of resource object #588 (comment)). - It would be useful to allow implementations to provide various links relevant to the given resource or response object, whether "verbs" or "nouns", whether representing actions on the endpoints of resource/its relationships/custom endpoints, or different aspects/views/versions/etc of the given resource, or anything else related to the given resource. Currently, only a very limited set of links is allowed (relationships, pagination, self), and
meta
is the only option for those who need more. It is workable, but it will drive fragmentation in the emerging JSON API ecosystem. - Provide an implementation of linking in JSON: allow association of RFC5988 Link Relations to all links. This must be optional in order not to affect adoption of the spec.
- Make links extendable: they are currently defined as strings, which does not allow to add any customization/context to them, either in basic spec in the future or via extensions today.
- While achieving all this, the subset of the spec which defines minimally required data structures should not become much more complicated than it currently is, so that adoption is not affected.
Proposed changes
Move relationships under resource-level key[This is implemented in Proposal forrelationships
, keepingself
under resource-level keylinks
. Relationships consist of relationship objects (formerly link objects) keyed with relationship name (Proposal for"relationships"
member at resource object top-level #618)."relationships"
member at resource object top-level #618, Relationships object #625]Relationship objects consist of 3 members:[This is implemented in Proposal forlinks
,linkage
,meta
(at least one is required)."relationships"
member at resource object top-level #618, Relationships object #625]Member[This is implemented in Proposal forlinks
has identical definition whether it occurs on top, resource, or relationship level."relationships"
member at resource object top-level #618, Relationships object #625]- Two forms of member
links
are possible: full (always available) or simplified (only available in special cases). - Full form of member
links
: an array, each element of it is a link object. - Define link object in the spec as an object having the following members:
name
: required, self-explanatory,href
: required, self-explanatory,class
: required, classification of link type. It is a CSV-string with elements from the following set:general,pagination,action,version,hierarchy,custom
(this set is obviously meant to be extendable in the future). Each link may have multiple classes. Elements of the set mean the following:general
(could not find a good name for it): for generally applicable links (possible values:self,related,type,copyright,help
),pagination
: includes pagination members in top-level and relationship-levellinks
(possible values:prev,next,first,last
),action
: actions on the given resource or relationships, available to the given user at the given moment of time, either at the resource's own endpoint or at some custom endpoint (possible values:create,update,delete,explore
; generalself
substitutes the action of reading the current resource/relationship -- we might want to try to eliminate this inconsistency),version
: links pointing at other versions of the given resource or providing versioning information about it (possible values:version-history,working-copy,successor-version,predecessor-version,latest-version,latest-published-version
, see RFC5988),hierarchy
: links for handling hierarchical content (possible values:up
, see RFC5988),custom
: all other (yet unclassified) links.
method
: optional, contains HTTP verb,rfc5988
: optional, allows to add RFC5988 Link Relations. If defined, it should contain an object consisting of members keyed by names oflink-param
s from RFC5988 or its extensions.URI-Reference
is considered to be set inhref
.
- Attributes share namespace with relationships but not with links.
- In each given member
links
, each link object is identified by a pairname
andclass
(canonicalized, e.g. with elements sorted alphabetically), so such pairs must be unique. (Alternatively,class
can be defined as array, e.g.,"class":["pagination","custom"]
: alphabetic reordering and serialization are needed for use as key.) - The spec should be able to reserve names of links of standard classes in order to have freedom for future extensions, but at the same time it would be useful if the spec allowed implementations to extend standard classes or define completely arbitrary links. To achieve that, the spec reserves possible ranges of values of
name
for link objects classified with one ofgeneral,pagination,action,version,hierarchy
. Link withclass
having any other value or any combination of valuesincluding any other value can have unrestrictedname
. E.g., if a link has"class":"pagination"
, it could only havename
as one ofprev,next,first,last
, but if it has"class":"pagination,custom"
or just"class":"custom"
, itsname
can be anything. All reserved values ofname
defined by spec must be unique, regardless of class. Proposed values per class to be defined by the spec are listed above in parentheses. - If member
links
only has link objects classified with single class, none of which iscustom
, and none of link objects has optional members, then suchlinks
can be returned by the server in simplified form: an object where values ofhref
are keyed by values ofname
(class
can be derived fromname
becausename
is unique for non-custom link classes). As of Relationships object #625, the simplified form is justa tiny bit more complex thanthe current version of the spec. Perhaps the full form can be described in a separate section of the base spec, not in the primary text flow of the spec. On another hand, support for full form could be an official extension, but IMHO it should be a part of the base spec. - In principle, link objects defined this way can be used anywhere in the response. E.g., the implementation can consider returning all attributes which contain URLs as link objects in full form.
- Top-level link with
"class":"action"
and nameexplore
could be used to communicate starting point for browsing a HATEOAS-compliant API.
Example
Illustrates all formats, both simple cases (article with ID=3) and complex cases (article with ID=2), dynamic permissions through actions (published article with ID=1 vs unpublished article with ID=2), RFC5988 mapping, custom actions (report for article with ID=1).
I am not suggesting to allow mix of different link forms in one response, it is here only to shorten the example.
GET /articles?page=1&page_size=3&related_page_size=2
{
"data": [{
"type": "articles",
"id": "1",
"attributes": {
"title": "JSON-LD paints my bikeshed!",
"published": "true"
},
"relationships": {
"authors": {
"links": {
"self": "http://example.com/articles/1/links/authors",
"related": "http://example.com/articles/1/authors",
},
"linkage": [
{ "type": "authors", "id": "3" },
{ "type": "authors", "id": "4" }
]
}
},
"links": [{
"name": "self",
"class": "general",
"href": "http://example.com/articles/1"
},{
"name": "citation-report",
"class": "custom",
"method": "get",
"href": "http://example.com/reports?type=citations&article_id=1",
"rfc5988": {
"rel": "http://example.com/notions/citation-reports",
"type": "application/pdf",
"title": "A report on citations of the given article over the last 5 years.
Data is derived from ISI Web of Science database in Computer Science."
}
}]
},{
"type": "articles",
"id": "2",
"attributes": {
"title": "JSON API paints my bikeshed!",
"published": "false"
},
"relationships": {
"authors": {
"links": [{
"name": "self",
"class": "general",
"href": "http://example.com/articles/2/links/authors",
"rfc5988": {
"rel": "self"
}
},{
"name": "related",
"class": "general",
"href": "http://example.com/articles/2/authors",
"method": "get",
"rfc5988": {
"rel": "related author"
}
},{
"name": "update",
"class": "action",
"href": "http://example.com/articles/2/links/authors",
"method": "patch"
},{
"name": "next",
"class": "pagination",
"href": "http://example.com/articles/2/authors?page=1&related_page_size=2",
"method": "get",
"rfc5988": {
"rel": "related author next"
}
},{
"name": "last",
"class": "pagination",
"href": "http://example.com/articles/2/authors?page=last&related_page_size=2",
"method": "get",
"rfc5988": {
"rel": "related author last"
}
}],
"linkage": [
{ "type": "authors", "id": "5" },
{ "type": "authors", "id": "12" }
],
"meta": {
"pagination": {
"related_page_size": "2",
"related_page_count": "3",
"related_item_count": "5"
}
}
}
},
"links": [{
"name": "self",
"class": "general",
"href": "http://example.com/articles/2"
},{
"name": "update",
"class": "action",
"method": "patch",
"href": "http://example.com/articles/2"
},{
"name": "delete",
"class": "action",
"method": "delete",
"href": "http://example.com/articles/2"
},{
"name": "publish",
"class": "action,custom",
"method": "post",
"href": "http://example.com/articles/2/publish"
},{
"name": "version-history",
"class": "version",
"method": "get",
"href": "http://example.com/articles/2/version-history",
"rfc5988": {
"rel": "version-history"
}
},{
"name": "version-drop-wip",
"class": "version,action,custom",
"method": "post",
"href": "http://example.com/articles/2/version-drop-wip",
"rfc5988": {
"rel": "http://example.com/notions/version-drop-wip"
}
},{
"name": "type",
"class": "general",
"href": "http://example.com/notions/articles",
"rfc5988": {
"rel": "type"
}
}]
},{
"type": "articles",
"id": "3",
"attributes": {
"title": "On Silver Bullets",
"published": "true"
},
"relationships": {
"authors": {
"links": {
"self": "http://example.com/articles/3/links/authors",
"related": "http://example.com/articles/3/authors"
},
"linkage": { "type": "authors", "id": "33" }
}
},
"links": {
"self": "http://example.com/articles/3"
}
}],
"links": [{
"name": "self",
"class": "general",
"href": "http://example.com/articles?page=1&page_size=3&related_page_size=2"
},{
"name": "explore",
"class": "action",
"href": "http://example.com/start-api-exploration"
},{
"name": "copyright",
"class": "general",
"href": "http://example.com/copyright",
"rfc5988": {
"rel": "copyright",
"type": "text/html"
}
},{
"name": "next",
"class": "pagination",
"href": "http://example.com/articles?page=2&page_size=3&related_page_size=2",
"rfc5988": {
"rel": "next"
}
},{
"name": "last",
"class": "pagination",
"href": "http://example.com/articles?page=last&page_size=3&related_page_size=2",
"rfc5988": {
"rel": "last"
}
}],
"meta": {
"pagination": {
"page_size": "3",
"page_count": "15",
"item_count": "43"
}
}