open-json-api

Open API & JSON API? O, ja!

Usage no npm install needed!

<script type="module">
  import openJsonApi from 'https://cdn.skypack.dev/open-json-api';
</script>

README

OJA: Open JSON API

Open API & JSON API? O, ja!

A library to make authoring JSON-API-compliant (v1) endpoints with Open API (née Swagger) specifications (v2) a little easier. Both are highly precise, powerful standards, but their verbosity can make it difficult to reap their full benefit without headache.

Installation

OJA can be installed over npm, and should be installed local to a project:

npm install open-json-api

API

The API snippets below all assume oja has been loaded using require, like so:

var oja = require('open-json-api')

oja.normalize(spec, name, obj)

Normalizes obj to a Resource based on the definition of name within spec. For example, given the name Widget with the following (abbreviated) spec:

{
  "type": "widgets",
  "id": "string",
  "attributes": {
    "name": "string",
    "price": "number",
  },
  "relationships": {
    "company": {
      "data": ""
    }
  }
}

Both of the following is true:

{                                {
  "id": 777,                       "type": "widgets",
  "name": "Example Widget",  =>    "id": "777",
  "price:" 42.99,                  "attributes": {
}                            =>      "name": "Example Widget",
                             =>      "price": 42.99,
                                   },
                                 }

{                                  {
  "type": "widgets",           =>    "type": "widgets",
  "id": "777",                 =>    "id": "777",
  "attributes": {              =>    "attributes": {
    "name": "Example Widget",  =>      "name": "Example Widget",
    "price": 42.99,            =>      "price": 42.99,
  },                                 },
}                                  }

oja.flatten(obj)

Flattens obj to a single namespace, independent of type. As with normalize Both converting from a Resource and passing in an already-flattened object should have the same result:

{                                  {
  "type": "widgets",           =>    "id": 777,
  "id": "777",                 =>    "name": "Example Widget",
  "attributes": {              =>    "price:" 42.99,
    "name": "Example Widget",      }
    "price": 42.99,
  },
}

{                                {
  "id": 777,                 =>    "id": 777,
  "name": "Example Widget",  =>    "name": "Example Widget",
  "price:" 42.99,            =>    "price:" 42.99,
}                                }

oja.initializeSpec(options)

Returns a valid top-level Open API specification with the provided options.title and options.version, if any. If not provided, defaults will be used.

Example:

oja.initializeSpec({ title: 'My Awesome API', version: 'beta' })
{
  "swagger": "2.0",
  "produces": ["application/json"],
  "info": {
    "title": "My Awesome API",
    "version": "beta"
  },
  "paths": {}
}

oja.defineResource(spec, name, options)

Adds definitions to spec suitable for validating and normalizing name Resources. The options Object takes the following format, though only type is required:

{
  type: 'examples',     // The value of "type" for this kind of Resource.
  attributes: {
    example_attr: {     // Keys within "attributes" become field names in the
                        // finished spec.
      type: 'string',   // Values should be valid Open API Schema Objects
                        // (A subset of JSON Schema).
    }
  },
  relationships: {
    example_rel:        // Keys within "relationships" become field names in
                        // the finished spec.
      'AnotherExample', // Values should correspond to the "name" you give
                        // other Resources (presumably using defineResource).
    example_coll:       // When building a one-to-many relationship,
      ['SubExample'],   // use an Array containing the other "name".
  }
}

More details on the attributes Schema Objects can be found in the Open API docs

Where is the serialize/deserialize method?

With enough cumulative time spent writing endpoints on both the producing and consuming side of JSONAPI, it turns out that the Resource format is the best canonical format for honoring the behaviour in the spec. Flattening the Resource (putting attributes keys, relationships keys, and id into the same namespace—typically removing type in the process) strips it, by definition, of all its semantic value.

  • Updates: In order to efficiently process PATCH requests on the server, we need to know what properties are being updated. Preserving the Resource format allows us to cleanly separate attribute updates from new relationships. Likewise, client-side, constructing a minimum update request is made easier by dividing the concerns between changed attributes and new edges in the graph.
  • Retrieval: Retaining a focus on Resources as the primary format for information similarly promotes the use of Resource Identifiers for retreival. Though outside the scope of this library, any code necessary for fetching a Resource (e.g. for a top-level UI component in React) can and should pull the Resource Identifier from relationships, passing that Identifier into the module responsible for getting, caching, etc.
  • Caching: The client-side logic for handling and caching data becomes trivial: get, cache, and render Resources. Typically, UI components pluck some subset of the attributes anyway.
  • Side-Loading: The server-side logic for side-loading is made simpler by not serializing Resources into top-level objects ; otherwise, includes have to be mixed into the result.

As a result of the above points (undoubtedly including more omitted due to a lapse of memory), the API for both incoming and outgoing Resources is made more explicit: normalize for ensuring an Object matches the expected Resource format for a given type of Resource, and flatten for explicitly converting the Resource into a POJO. The latter should be used sparingly, but is useful, for example, for loading Resources into ORMs that expect flattened objects.

Potential surprises

  • Unlike jsonapi-serializer, OJA does not automatically fill out included. Instead, individual Resources should be explicitly included, such that the business logic explicitly handles all cases where only a partial, related Resource is currently available.