a node.js
library for serializing your data to JSON API v1.0 compliant documents, inspired by jsonapi-serializer. this library makes no assumptions regarding your choice of ORM/ODM, or the structure of your data. simply define your types and how their related and let this library do the heavy lifting.
npm install --save json-api-ify
Create a new reusable serializer.
var Serializer = require('json-api-ify');
let serializer = new Serializer({
baseUrl: 'https://www.example.com/api',
topLevelMeta: {
'api-version': 'v1.0.0'
}
});
Define a type. (read more about options below)
serializer.define('users', {
id: '_id',
blacklist: [
'password',
'phone.mobile'
],
links: {
self(resource, options, cb) {
let link = options.baseUrl + '/users/' + resource.id;
cb(null, link);
}
},
meta: {
nickname(resource, options, cb) {
let nickname = 'lil ' + resource.attributes.first;
cb(null, nickname);
}
},
processResource(resource, cb) {
return cb(null, resource.toObject());
},
topLevelLinks: {
self(options, cb) {
let link = options.baseUrl + '/users';
cb(null, link);
},
next(options, cb) {
let link = options.baseUrl + '/users';
if (options.nextPage) {
link += '?page=' + options.nextPage;
}
cb(null, link);
}
},
topLevelMeta: {
total(options, cb) {
cb(null, options.total);
}
}
}, function(err) {
// check for definition errors
})
Get a hold of some data that needs to be serialized.
let data = [new User({
first: 'Kendrick',
last: 'Lamar',
email: 'klamar@example.com',
password: 'elkjqe0920oqhvrophepohiwveproihgqp398yr9pq8gehpqe9rf9q8er',
phone: {
home: '+18001234567',
mobile: '+180045678910'
},
address: {
addressLine1: '406 Madison Court',
zipCode: '49426',
country: 'USA'
}
}), new User({
first: 'Kanye',
last: 'West',
email: 'kwest@example.com',
password: 'asdlkj2430r3r0ghubwf9u3rbg9u3rbgi2q3oubgoubeqfnpviquberpibq',
phone: {
home: '+18002345678',
mobile: '+18007890123'
},
address: {
addressLine1: '361 Shady Lane',
zipCode: '23185',
country: 'USA'
}
})];
Serialize it
serializer.serialize('users', data, function(err, payload) {
console.log(payload);
});
Or, use it in a route
function(req, res) {
async.auto({
users: function findUsers(fn) {
User.find({})
.limit(10)
.skip(parseInt(req.query.page || 0) * 10)
.exec(fn);
},
count: function countUsers(fn) {
User.count({}).exec(fn);
},
payload: ['users', 'count', function serialize(fn, results) {
serializer.serialize('users', results.users, {
total: results.count,
nextPage: (req.query.page || 1) + 1
}, fn);
}]
}, function(err, payload) {
if (err) {
return res.json(500, {errors: [{
status: 500,
detail: err.message
}]});
}
res.json(200, payload);
});
}
Response body:
{
"links": {
"self": "https://www.example.com/api/users",
"next": "https://www.example.com/api/users?page=2"
},
"data": [
{
"type": "users",
"id": "54735750e16638ba1eee59cb",
"attributes": {
"first": "Kendrick",
"last": "Lamar",
"email": "klamar@example.com",
"phone": {
"home": "+18001234567"
},
"address": {
"addressLine1": "406 Madison Court",
"zipCode": "49426",
"country": "USA"
}
},
"relationships": {},
"links": {
"self": "https://www.example.com/api/users/54735750e16638ba1eee59cb"
},
"meta": {
"nickname": "lil Kendrick"
}
},
{
"type": "users",
"id": "5490143e69e49d0c8f9fc6bc",
"attributes": {
"first": "Kanye",
"last": "West",
"email": "kwest@example.com",
"phone": {
"home": "+18002345678"
},
"address": {
"addressLine1": "361 Shady Lane",
"zipCode": "23185",
"country": "USA"
}
},
"relationships": {},
"links": {
"self": "https://www.example.com/api/users/5490143e69e49d0c8f9fc6bc"
},
"meta": {
"nickname": "lil Kanye"
}
}
],
"included": [],
"meta": {
"api-version": "v1.0.0",
"total": 2
}
}
A type can have multiple serialization schemas, which you can create by calling define
with a schema name. Any schema options provided will augment the default schema.
serializer.define('users', 'names-only', {
whitelist: [
'first',
'last'
]
}, callback);
serializer.serialize('users', 'names-only', data, function(err, payload) {
console.log(payload);
});
{
"links": {
"self": "https://www.example.com/api/users"
},
"data": [
{
"type": "users",
"id": "54735750e16638ba1eee59cb",
"attributes": {
"first": "Kendrick",
"last": "Lamar"
},
"relationships": {},
"links": {
"self": "https://www.example.com/api/users/54735750e16638ba1eee59cb"
},
"meta": {
"nickname": "lil Kendrick"
}
},
{
"type": "users",
"id": "5490143e69e49d0c8f9fc6bc",
"attributes": {
"first": "Kanye",
"last": "West"
},
"relationships": {},
"links": {
"self": "https://www.example.com/api/users/5490143e69e49d0c8f9fc6bc"
},
"meta": {
"nickname": "lil Kanye"
}
}
],
"included": [],
"meta": {
"api-version": "v1.0.0"
}
}
Relationships are easy as well. First, include a relationship map in your type/schema options.
serializer.define('users', {
// ..
relationships: {
groups: {
type: 'groups',
include: true,
links: {
self(resource, options, cb) {
let link = options.baseUrl + '/users/' + resource.id + '/relationships/groups';
cb(null, link);
},
related(resource, options, cb) {
let link = options.baseUrl + '/users/' + resource.id + '/groups';
cb(null, link);
}
}
}
}
// ..
}, callback);
Lastly, define the related type.
serializer.define('groups', {
// ..
relationships: {
users: {
type: 'users',
include: true,
schema: 'names-only',
links: {
self(resource, options, cb) {
let link = options.baseUrl + '/groups/' + resource.id + '/relationships/users';
cb(null, link);
},
related(resource, options, cb) {
let link = options.baseUrl + '/groups/' + resource.id + '/users';
cb(null, link);
}
}
}
}
// ..
}, callback);
extract the data from a payload in a slightly more usable fashion
let payload = {
data: {
type: 'user',
attributes: {
first: 'a$ap',
last: 'ferg',
email: 'aferg@example.com',
phone: {
home: '1-111-111-1111'
}
},
relationships: {
groups: {
data: [{
type: 'group',
id: '56cd74546033f8d420bc1c11'
},{
type: 'group',
id: '56cd74546033f8d420bc1c12'
}]
}
}
}
};
serializer.deserialize(payload, function(err, data) { /* .. */ });
here, data would look like:
{
"user": {
"first": "a$ap",
"last": "ferg",
"email": "aferg@example.com",
"phone": {
"home": "1-111-111-1111"
},
"groups": [{
"_id": "56cd74546033f8d420bc1c11"
},{
"_id": "56cd74546033f8d420bc1c12"
}]
},
"groups": [{
"_id": "56cd74546033f8d420bc1c11"
},{
"_id": "56cd74546033f8d420bc1c12"
}]
}
constructs a new serializer instance
Param | Type | Description |
---|---|---|
[options] |
{Object} |
global options. see serialize() options for more detail |
defines a type serialization schema
Param | Type | Description |
---|---|---|
type |
{String} |
the resource type |
[schema] |
{String} |
the serialization schema to use. defaults to default |
options |
{Object} |
schema options |
callback(err) |
{Function} |
a function that receives any definition error. |
deserializes the data attribute of the payload
Param | Type | Description |
---|---|---|
payload |
{Object} |
a valid JSON API payload |
callback(err, data) |
{Function} |
a function that receives any deserialization error and the extracted data. |
serializes data
into a JSON API v1.0 compliant document
Param | Type | Description |
---|---|---|
type |
{String} |
the resource type |
[schema] |
{String} |
the serialization schema to use. defaults to default |
data |
{*} |
the data to serialize |
[options] |
{Object} |
single use options. these options will be merged with the global options, default schema options, and any applicable non-default schema options |
callback(err, payload) |
{Function} |
a function that receives any serialization error and JSON API document. |
{
// an array of string paths to omit from the resource, this option
// includes relationships that you may wish to omit
blacklist: [],
// the path to the primary key on the resource
id: 'id',
// a map of resource links
links: {
// asynchronous
self(resource, options, cb) {
// each key can be a value to set, or asynchronous function that
// receives the processed resource, serialization options, and
// a callback that should pass any error and the link value
cb(null, link);
},
// synchronous
self(resource, options) {
return options.baseUrl + '/api/users/' + resource.id;
}
},
// a map of meta members
meta: {
// asynchronous
self(resource, options, cb) {
// each key can be a value to set, or asynchronous function that
// receives the processed resource, serialization options, and
// a callback that should pass any error and the meta value
cb(null, meta);
},
// synchronous
self(resource, options) {
return meta;
}
},
// preprocess your resources
// all resources must be objects, otherwise they're assumed to be
// unpopulated ids. NOTE!! If you're working with mongoose models,
// unpopulated ids can be objects, so you will need to convert them
// to strings
processResource(resource, /* cb */) {
if (typeof resource.toJSON === 'function') {
resource = resource.toJSON();
} else if (resource instanceof mongoose.Types.ObjectId) {
resource = resource.toString();
}
return resource;
},
// relationship configuration
relationships: {
// each key represents a resource path that points to a
// nested resource or collection of nested resources
'groups': {
// the type of resource
type: 'groups',
// whether or not to include the nested resource(s)
include: true,
// optionally specify a non-default schema to use
schema: 'my-schema',
// a map of links to define on the relationship object
links: {
self(resource, options, cb) {
},
related(resource, options, cb) {
}
}
}
},
// a map of top-level links
topLevelLinks: {
self(options, cb) {
}
},
// a map of top-level meta members
meta: {
total(options, cb) {
}
},
// an array of string paths to pick from the resource. this option
// overrides any specified blacklist and also includes relationships
whitelist: [],
}
serializes any error
into a JSON API v1.0 compliant error document. error can be anything, this method will attempt to intelligently construct a valid JSON API error object. the return document will contain a top level meta
member with a status
attribute that represents the status code with the greatest frequency.
Param | Type | Description |
---|---|---|
error |
{*} |
the error data to serialize |
[meta] |
{Object} |
any top level meta information |
[defaultStatusCode] |
`{Number | String}` |
function(req, res) {
async.waterfall([
// ..
], function(err, payload) {
let status = 200;
if (err) {
payload = serializer.serializeError(err);
status = payload.meta.status;
}
res.json(status, payload);
});
}
The serializer
inherits from node's EventEmitter
. Below is a summary of the events exposed by this library.
The global error event.
Param | Type | Description |
---|---|---|
error |
{Object} |
the error object |
serializer.on('error', function(error) {
bugsnag.notify(error);
});
- implement
jsonapi
top-level member - implement
deserialize
method - implement support for unpopulated relationships (an id, or array of ids)
- implement templates
- ADD MORE TESTS!
run tests
npm test
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
Copyright (c) 2016 Chris Ludden.
Licensed under the MIT license.