Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Links, relationships, discoverable actions, mapping to RFC5988 #622

Closed
@hhware

Description

@hhware

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
  1. 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.
  2. 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.
  3. 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-level links and resource-level self are singular links, so they are not similar at all, one is a actually a component of another. Another aspect of it is making all links 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)).
  4. 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.
  5. 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.
  6. 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.
  7. 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
  1. Move relationships under resource-level key relationships, keeping self under resource-level key links. Relationships consist of relationship objects (formerly link objects) keyed with relationship name (Proposal for "relationships" member at resource object top-level #618). [This is implemented in Proposal for "relationships" member at resource object top-level #618, Relationships object #625]
  2. Relationship objects consist of 3 members: links, linkage, meta (at least one is required). [This is implemented in Proposal for "relationships" member at resource object top-level #618, Relationships object #625]
  3. Member links has identical definition whether it occurs on top, resource, or relationship level. [This is implemented in Proposal for "relationships" member at resource object top-level #618, Relationships object #625]
  4. Two forms of member links are possible: full (always available) or simplified (only available in special cases).
  5. Full form of member links: an array, each element of it is a link object.
  6. Define link object in the spec as an object having the following members:
    1. name: required, self-explanatory,
    2. href: required, self-explanatory,
    3. 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:
      1. general (could not find a good name for it): for generally applicable links (possible values: self,related,type,copyright,help),
      2. pagination: includes pagination members in top-level and relationship-level links (possible values: prev,next,first,last),
      3. 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; general self substitutes the action of reading the current resource/relationship -- we might want to try to eliminate this inconsistency),
      4. 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),
      5. hierarchy: links for handling hierarchical content (possible values: up, see RFC5988),
      6. custom: all other (yet unclassified) links.
    4. method: optional, contains HTTP verb,
    5. rfc5988: optional, allows to add RFC5988 Link Relations. If defined, it should contain an object consisting of members keyed by names of link-params from RFC5988 or its extensions. URI-Reference is considered to be set in href.
  7. Attributes share namespace with relationships but not with links.
  8. In each given member links, each link object is identified by a pair name and class (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.)
  9. 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 of general,pagination,action,version,hierarchy. Link with class having any other value or any combination of values including any other value can have unrestricted name. E.g., if a link has "class":"pagination", it could only have name as one of prev,next,first,last, but if it has "class":"pagination,custom" or just "class":"custom", its name can be anything. All reserved values of name defined by spec must be unique, regardless of class. Proposed values per class to be defined by the spec are listed above in parentheses.
  10. If member links only has link objects classified with single class, none of which is custom, and none of link objects has optional members, then such links can be returned by the server in simplified form: an object where values of href are keyed by values of name (class can be derived from name because name is unique for non-custom link classes). As of Relationships object #625, the simplified form is just a tiny bit more complex than the 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.
  11. 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.
  12. Top-level link with "class":"action" and name explore 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"
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions