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.
OJA can be installed over npm, and should be installed local to a project:
npm install open-json-api
The API snippets below all assume oja
has been loaded using require
, like
so:
var oja = require('open-json-api')
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,
}, },
} }
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,
} }
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": {}
}
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
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.
- Unlike
jsonapi-serializer
, OJA does not automatically fill outincluded
. 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.