Hypermedia API with cubicweb-jsonschema

This is the second post of a series about cubicweb-jsonschema. The first post mainly dealt with JSON Schema representations of CubicWeb entities along with a brief description of the JSON API. In this second post, I'll describe another aspect of the project that aims at building an hypermedia API by leveraging the JSON Hyper Schema specification.

Hypermedia APIs and JSON Hyper Schema

Hypermedia API is somehow a synonymous of RESTful API but it makes it clearer that the API serves hypermedia responses, i.e. content that helps discoverability of other resources.

At the heart of an hypermedia API is the concept of link relation which both aims at describing relationships between resources as well as provinding ways to manipulate them.

In JSON Hyper Schema terminology, link relations take the form of a collection of Link Description Objects gathered into a links property of a JSON Schema document. These Link Description Objects thus describes relationships between the instance described by the JSON Schema document at stake and other resources; they hold a number of properties that makes relationships manipulation possible:

  • rel is the name of the relation, it is usually one of relation names registered at IANA;
  • href indicates the URI of the target of the relation, it may be templated by a JSON Schema;
  • targetSchema is a JSON Schema document (or reference) describing the target of the link relation;
  • schema (recently renamed as submissionSchema) is a JSON Schema document (or reference) describing what the target of the link expects when submitting data.

Hypermedia walkthrough

In the remaining of the article, I'll walk through a navigation path that is made possible by hypermedia controls provided by cubicweb-jsonschema. I'll continue on the example application described in the first post of the series which schema consists of Book, Author and Topic entity types. In essence, this walkthrough is typical of what an intelligent client could do when exposed to the API, i.e. from any resource, discover other resources and navigate or manipulate them.

This walkthrough assumes that, given any resource (i.e. something that has a URL like /book/1), the server would expose data at the main URL when the client asks for JSON through the Accept header and it would expose the JSON Schema of the resource at a schema view of the same URL (i.e. /book/1/schema). This assumption can be regarded as a kind of client/server coupling, which might go away in later implementation.

Site root

While client navigation could start from any resource, we start from the root resource and retrieve its schema:

GET /schema Accept: application/schema+json

HTTP/1.1 200 OK Content-Type: application/json

{ "links": [ { "href": "/author/", "rel": "collection", "schema": { "$ref": "/author/schema?role=creation" }, "targetSchema": { "$ref": "/author/schema" }, "title": "Authors" }, { "href": "/book/", "rel": "collection", "schema": { "$ref": "/book/schema?role=creation" }, "targetSchema": { "$ref": "/book/schema" }, "title": "Books" }, { "href": "/topic/", "rel": "collection", "schema": { "$ref": "/topic/schema?role=creation" }, "targetSchema": { "$ref": "/topic/schema" }, "title": "Topics" } ] }

So at root URL, our application serves a JSON Hyper Schema that only consists of links. It has no JSON Schema document, which is natural since there's usually no data bound to the root resource (think of it as empty rset in CubicWeb terminology).

These links correspond to top-level entity types, i.e. those that would appear in the default startup page of a CubicWeb application. They all have "rel": "collection" relation name (this comes from RFC6573) as their target is a collection of entities. We also have schema and targetSchema properties.

From collection to items

Now that we have added a new book, let's step back and use our books link to retrieve data (verb GET):

GET /book/ Accept: application/json

HTTP/1.1 200 OK Allow: GET, POST Content-Type: application/json

[ { "id": "859", "title": "L'homme qui rit" }, { "id": "858", "title": "The Old Man and the Sea" }, ]

which, as always, needs to be completed by a JSON Schema:

GET /book/schema Accept: application/schema+json

HTTP/1.1 200 OK Content-Type: application/json

{ "$ref": "#/definitions/Book_plural", "definitions": { "Book_plural": { "items": { "properties": { "id": { "type": "string" }, "title": { "type": "string" } }, "type": "object" }, "title": "Books", "type": "array" } }, "links": [ { "href": "/book/", "rel": "collection", "schema": { "$ref": "/book/schema?role=creation" }, "targetSchema": { "$ref": "/book/schema" }, "title": "Books" }, { "href": "/book/{id}", "rel": "item", "targetSchema": { "$ref": "/book/schema?role=view" }, "title": "Book" } ] }

Consider the last item of links in the above schema. It has a "rel": "item" property which indicates how to access items of the collection; its href property is a templated URI which can be expanded using instance data and schema (here we only have a single id template variable).

So our client may navigate to the first item of the collection (id="859") at /book/859 URI, and retrieve resource data:

GET /book/859 Accept: application/json

HTTP/1.1 200 OK Allow: GET, PUT, DELETE Content-Type: application/json

{ "author": [ "Victor Hugo" ], "publication_date": "1869-04-01T00:00:00", "title": "L'homme qui rit" }

and schema:

GET /book/859/schema Accept: application/schema+json

HTTP/1.1 200 OK Content-Type: application/json

{ "$ref": "#/definitions/Book", "definitions": { "Book": { "additionalProperties": false, "properties": { "author": { "items": { "type": "string" }, "title": "author", "type": "array" }, "publication_date": { "format": "date-time", "title": "publication date", "type": "string" }, "title": { "title": "title", "type": "string" }, "topics": { "items": { "type": "string" }, "title": "topics", "type": "array" } }, "title": "Book", "type": "object" } }, "links": [ { "href": "/book/", "rel": "up", "targetSchema": { "$ref": "/book/schema" }, "title": "Book_plural" }, { "href": "/book/859/", "rel": "self", "schema": { "$ref": "/book/859/schema?role=edition" }, "targetSchema": { "$ref": "/book/859/schema?role=view" }, "title": "Book #859" } ] }

Entity resource

The resource obtained above as an item of a collection is actually an entity. Notice the rel="self" link. It indicates how to manipulate the current resource (i.e. at which URI, using a given schema depending on what actions we want to perform). Still this link does not indicate what actions may be performed. This indication is found in the Allow header of the data response above:

Allow: GET, PUT, DELETE

With these information bits, our intelligent client is able to, for instance, form a request to delete the resource. On the other hand, the action to update the resource (which is allowed because of the presence of PUT in Allow header, per HTTP semantics) would take the form of a request which body conforms to the JSON Schema pointed at by the schema property of the link.

Also note the rel="up" link which makes it possible to navigate to the collection of books.

Conclusions

This post introduced the main hypermedia capabilities of cubicweb-jsonschema, built on top of the JSON Hyper Schema specification. The resulting Hypermedia API makes it possible for an intelligent client to navigate through hypermedia resources and manipulate them by using both link relation semantics and HTTP verbs.

In the next post, I'll deal with relationships description and manipulation both in terms of API (endpoints) and hypermedia representation.