Skip to content

Instantly share code, notes, and snippets.

@ericgj
Last active April 5, 2020 22:50
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ericgj/7646961 to your computer and use it in GitHub Desktop.
Save ericgj/7646961 to your computer and use it in GitHub Desktop.
Implementing a REST workflow using JSON Schema, an example

Implementing a REST workflow using JSON Schema, an example

The problem domain

You are designing a document-control system. Users create and edit documents for publication. Documents can be draft, in review, on hold, in process, or published. Users have specific roles which determine which documents they have access to, and what they are permitted to do to the documents, as the documents move through the workflow. Some users are writers, others are editors.

Expressed in plain language, the rules are:

  • Writers can view their own documents regardless of status.
  • Writers can create new documents.
  • Writers can save new documents as draft, or to in review if certain information is included.
  • Writers can edit draft documents.
  • Writers can move draft documents to in review if certain information is included.
  • Editors can view all draft documents.
  • Editors can view and edit all in review, on hold, in process, or published documents.
  • Editors can move in review documents to on hold, in process, or published.
  • Editors can move on hold documents back to the previous status they were before being placed on hold.
  • Editors can move in process documents to published or on hold.

REpresentational State Transfer

In general, a REST style architecture allows you to express transitions (between states of a single entity or between entities) as links.

If you imagine a typical UI that instantiates the rules above, as an editor or writer you are presented with a set of links or buttons (or menu of choices in a CLI) depending on your role and the state of the loaded document or documents. Each of these links triggers an HTTP action. The action can be preceded by validation of so-called guard conditions. For instance: in our case, when a writer attempts to move a document to in review, certain information must be included, or the transition fails.

A problem for any such system is how the client-side of the application knows about the state transition logic, ie. what links are available under what conditions, what actions they trigger, and what guard conditions must be checked.

Until we reach the magical 'no-backend' future, where the client originates this logic and configures the server accordingly, our basic choices are:

  1. Duplicate the logic in server and client
  2. Split the logic between server and client
  3. Rely on shared conventions between server and client (same as #1)
  4. Share code between server and client (if you have a node.js backend)
  5. Implement all the logic on the client (including transpiling your application from some other language to js)
  6. The server encodes the logic and sends it to the client.

This little write-up is to demonstrate how JSON Schema provides the standards for implementing this last solution: which is the most elegant and flexible, not to mention the one that the REST style was designed to provide.

JSON Schema

The JSON Schema specs are somewhat complex, but worth reading through to familiarize yourself with the basic concepts, particularly in the hyperschema spec.

There are several key innovations that JSON Schema provides, that are important to understand here before diving in further.

  • JSON Schema is not a particular data format. It puts no limitations on the format of your message bodies between client and server. The schema data, including links, are communicated via separate HTTP calls, and correlated with particular data messages (or "instances") via HTTP headers (and links).

This is very important in terms of agility, because it means your client application is not locked in to a particular data format up front, and you can develop your schemas organically with your data requirements. In my opinion, not having this flexibility has been one of the major factors in preventing widespread adoption of the so-called HATEOAS constraint (Hypertext As The Engine Of Application State) for RESTful applications.

  • JSON References are used to provide composition of schemas. Schemas or fragments of schemas can be referenced within other schemas, and the referenced schemas can be in the same or different files/URI endpoints.

This is important because it means that schemas can be efficiently defined as serialized JSON, without duplication. (Also, as we will see, it allows schemas to be served as static files, taking advantage of built-in HTTP caching.)

  • URI templates are used to generate link addresses based on instance data. If an instance is not valid according to its schema, none of the schema's links are available. Also, if a value used in URI templates is not present in the data, the link is not available.

  • Multiple schemas can be correlated with instance data. Among other things, this allows composition of links based on multiple conditions.

First steps

Looking at the rules, some basic patterns emerge. There are overlapping conditions based on current document state and user role, but mostly they are distinct by user role. Also, it appears the target state of a transition determines the guard conditions, not the current state or user role.

But first, let's isolate one simple rule that applies universally: draft documents can be edited by anyone. A writer might not have access to someone else's draft document, but all documents that they do have access to, they can edit.

We may not ultimately decide to isolate this rule in this way, but it is a good place to start because it defers the question of user role in modeling the transitions.

Assuming that the document record has a "status" property which stores the current state, here is a representation of this rule using JSON Schema:

{
  "links": [
    { "rel": "edit" }
  ],
  "properties": {
    "status": { "enum": ["draft"] }
  }
}

What this achieves is to define a condition on the data (in this case, on the value of the status property). If the data meets this condition, the "edit" link is available; if not, no edit link is available under this schema.

It is important to remember that schemas are not exclusive: if there is another schema describing the data, and the data is valid under it, an edit link may be available through it instead. We will come to this shortly.

In the meantime, this schema is not quite complete: there is no URI and HTTP method given for the edit link. This is a lower-level decision we do not have to make now, but to put something in for now, using a typical convention:

{
  "links": [
    { "rel": "edit", "href": "/documents/{id}", "method": "PUT" }
  ],
  "properties": {
    "status": { "enum": ["draft"] }
  }
}

Note the use of the URI template for the href (URI). The template is rendered using the instance data. So assuming that the instance data is valid (status == "draft"), and that it has a value for "id", the edit link will be available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment