/json-api-serializer

Node.js framework agnostic JSON API (http://jsonapi.org/) serializer.

Primary LanguageJavaScriptMIT LicenseMIT

json-api-serializer

Build Status Coverage Status npm

A Node.js framework agnostic library for serializing your data to JSON API compliant responses (a specification for building APIs in JSON).

Installation

npm install --save json-api-serializer

Documentation

Register

var JSONAPISerializer = require("json-api-serializer");
var Serializer = new JSONAPISerializer();
Serializer.register(type, options);

Serialization options:

  • id (optional): The key to use as the reference. Default = 'id'.
  • blacklist (optional): An array of blacklisted attributes. Default = [].
  • whitelist (optional): An array of whitelisted attributes. Default = [].
  • jsonapiObject (optional): Enable/Disable JSON API Object. Default = true.
  • links (optional): Describes the links inside data. It can be:
    • An object (values can be string or function).
    • A function with one argument function(data) { ... } or with two arguments function(data, extraData) { ... }
  • topLevelMeta (optional): Describes the top-level meta. It can be:
    • An object (values can be string or function).
    • A function with one argument function(extraData) { ... } or with two arguments function(data, extraData) { ... }
  • topLevelLinks (optional): Describes the top-level links. It can be:
    • An object (values can be string or function).
    • A function with one argument function(extraData) { ... } or with two arguments function(data, extraData) { ... }
  • meta (optional): Describes resource-level meta. It can be:
    • An object (values can be string or function).
    • A function with one argument function(data) { ... } or with two arguments function(data, extraData) { ... }
  • relationships (optional): An object defining some relationships
    • relationship: The property in data to use as a relationship
      • type: A string or a function function(relationshipData, data) { ... } for the type to use for serializing the relationship (type need to be register).
      • alternativeKey (optional): An alternative key (string or path) to use if relationship key not exist (example: 'author_id' as an alternative key for 'author' relationship). See issue #12.
      • schema (optional): A custom schema for serializing the relationship. If no schema define, it use the default one.
      • links (optional): Describes the links for the relationship. It can be:
        • An object (values can be string or function).
        • A function with one argument function(data) { ... } or with two arguments function(data, extraData) { ... }
      • meta (optional): Describes meta that contains non-standard meta-information about the relationship. It can be:
        • An object (values can be string or function).
        • A function with one argument function(data) { ... } or with two arguments function(data, extraData) { ... }
      • deserialize (optional): Describes the function which should be used to deserialize a related property which is not included in the JSON:API document. It should be:
        • A function with one argument function(data) { ... }which defines the format to which a relation should be deserialized. By default, the ID of the related object is returned, which would be equal to function(data) {return data.id}. See issue #65.
  • convertCase (optional): Case conversion for serializing data. Value can be : kebab-case, snake_case, camelCase

Deserialization options:

  • unconvertCase (optional): Case conversion for deserializing data. Value can be : kebab-case, snake_case, camelCase
  • blacklistOnDeserialize (optional): An array of blacklisted attributes. Default = [].
  • whitelistOnDeserialize (optional): An array of whitelisted attributes. Default = [].

Global options:

To avoid repeating the same options for each type, it's possible to add global options on JSONAPISerializer instance:

var JSONAPISerializer = require("json-api-serializer");
var Serializer = new JSONAPISerializer({
  convertCase: "kebab-case",
  unconvertCase: "camelCase"
});

Usage

input data (can be an object or an array of objects)

// Data
var data = [
  {
    id: "1",
    title: "JSON API paints my bikeshed!",
    body: "The shortest article. Ever.",
    created: "2015-05-22T14:56:29.000Z",
    updated: "2015-05-22T14:56:28.000Z",
    author: {
      id: "1",
      firstName: "Kaley",
      lastName: "Maggio",
      email: "Kaley-Maggio@example.com",
      age: "80",
      gender: "male"
    },
    tags: ["1", "2"],
    photos: [
      "ed70cf44-9a34-4878-84e6-0c0e4a450cfe",
      "24ba3666-a593-498c-9f5d-55a4ee08c72e",
      "f386492d-df61-4573-b4e3-54f6f5d08acf"
    ],
    comments: [
      {
        _id: "1",
        body: "First !",
        created: "2015-08-14T18:42:16.475Z"
      },
      {
        _id: "2",
        body: "I Like !",
        created: "2015-09-14T18:42:12.475Z"
      },
      {
        _id: "3",
        body: "Awesome",
        created: "2015-09-15T18:42:12.475Z"
      }
    ]
  }
];

Register

Register your resources types :

var JSONAPISerializer = require("json-api-serializer");
var Serializer = new JSONAPISerializer();

// Register 'article' type
Serializer.register("article", {
  id: "id", // The attributes to use as the reference. Default = 'id'.
  blacklist: ["updated"], // An array of blacklisted attributes. Default = []
  links: {
    // An object or a function that describes links.
    self: function(data) {
      // Can be a function or a string value ex: { self: '/articles/1'}
      return "/articles/" + data.id;
    }
  },
  relationships: {
    // An object defining some relationships.
    author: {
      type: "people", // The type of the resource
      links: function(data) {
        // An object or a function that describes Relationships links
        return {
          self: "/articles/" + data.id + "/relationships/author",
          related: "/articles/" + data.id + "/author"
        };
      }
    },
    tags: {
      type: "tag"
    },
    photos: {
      type: "photo"
    },
    comments: {
      type: "comment",
      schema: "only-body" // A custom schema
    }
  },
  topLevelMeta: function(data, extraData) {
    // An object or a function that describes top level meta.
    return {
      count: extraData.count,
      total: data.length
    };
  },
  topLevelLinks: {
    // An object or a function that describes top level links.
    self: "/articles" // Can be a function (with extra data argument) or a string value
  }
});

// Register 'people' type
Serializer.register("people", {
  id: "id",
  links: {
    self: function(data) {
      return "/peoples/" + data.id;
    }
  }
});

// Register 'tag' type
Serializer.register("tag", {
  id: "id"
});

// Register 'photo' type
Serializer.register("photo", {
  id: "id"
});

// Register 'comment' type with a custom schema
Serializer.register("comment", "only-body", {
  id: "_id"
});

Serialize

Serialize it with the corresponding resource type, data and optional extra data :

// Synchronously (blocking)
const result = Serializer.serialize('article', data, {count: 2});

// Asynchronously (non-blocking)
Serializer.serializeAsync('article', data, {count: 2})
  .then((result) => {
    ...
  });

The output data will be :

{
  "jsonapi": {
    "version": "1.0"
  },
  "meta": {
    "count": 2,
    "total": 1
  },
  "links": {
    "self": "/articles"
  },
  "data": [{
    "type": "article",
    "id": "1",
    "attributes": {
      "title": "JSON API paints my bikeshed!",
      "body": "The shortest article. Ever.",
      "created": "2015-05-22T14:56:29.000Z"
    },
    "relationships": {
      "author": {
        "data": {
          "type": "people",
          "id": "1"
        },
        "links": {
          "self": "/articles/1/relationships/author",
          "related": "/articles/1/author"
        }
      },
      "tags": {
        "data": [{
          "type": "tag",
          "id": "1"
        }, {
          "type": "tag",
          "id": "2"
        }]
      },
      "photos": {
        "data": [{
          "type": "photo",
          "id": "ed70cf44-9a34-4878-84e6-0c0e4a450cfe"
        }, {
          "type": "photo",
          "id": "24ba3666-a593-498c-9f5d-55a4ee08c72e"
        }, {
          "type": "photo",
          "id": "f386492d-df61-4573-b4e3-54f6f5d08acf"
        }]
      },
      "comments": {
        "data": [{
          "type": "comment",
          "id": "1"
        }, {
          "type": "comment",
          "id": "2"
        }, {
          "type": "comment",
          "id": "3"
        }]
      }
    },
    "links": {
      "self": "/articles/1"
    }
  }],
  "included": [{
    "type": "people",
    "id": "1",
    "attributes": {
      "firstName": "Kaley",
      "lastName": "Maggio",
      "email": "Kaley-Maggio@example.com",
      "age": "80",
      "gender": "male"
    },
    "links": {
      "self": "/peoples/1"
    }
  }, {
    "type": "comment",
    "id": "1",
    "attributes": {
      "body": "First !"
    }
  }, {
    "type": "comment",
    "id": "2",
    "attributes": {
      "body": "I Like !"
    }
  }, {
    "type": "comment",
    "id": "3",
    "attributes": {
      "body": "Awesome"
    }
  }]
}

Some others examples are available in tests folders

Deserialize

input data (can be an simple object or an array of objects)

var data = {
  data: {
    type: 'article',
    id: '1',
    attributes: {
      title: 'JSON API paints my bikeshed!',
      body: 'The shortest article. Ever.',
      created: '2015-05-22T14:56:29.000Z'
    },
    relationships: {
      author: {
        data: {
          type: 'people',
          id: '1'
        }
      },
      comments: {
        data: [{
          type: 'comment',
          id: '1'
        }, {
          type: 'comment',
          id: '2'
        }]
      }
    }
  }
};

// Synchronously (blocking)
Serializer.deserialize('article', data);

// Asynchronously (non-blocking)
Serializer.deserializeAsync('article', data)
  .then((result) => {
    // ...
  });
{
  "id": "1",
  "title": "JSON API paints my bikeshed!",
  "body": "The shortest article. Ever.",
  "created": "2015-05-22T14:56:29.000Z",
  "author": "1",
  "comments": [
    "1",
    "2"
  ]
}

serializeError

Serializes any error into a JSON API error document.

Input data can be:

const error = new Error('An error occured');
error.status = 500;

Serializer.serializeError(error);

The result will be:

{
  "errors": [
    {
      "status": "500",
      "detail": "An error occured"
    }
  ]
}

Custom schemas

It is possible to define multiple custom schemas for a resource type :

Serializer.register(type, "customSchema", options);

Then you can apply this schema on the primary data when serialize or deserialize :

Serializer.serialize("article", data, "customSchema", { count: 2 });
Serializer.serializeAsync("article", data, "customSchema", { count: 2 });
Serializer.deserialize("article", jsonapiData, "customSchema");
Serializer.deserializeAsync("article", jsonapiData, "customSchema");

Or if you want to apply this schema on a relationship data, define this schema on relationships options with the key schema :

Example :

relationships: {
  comments: {
    type: "comment";
    schema: "customSchema";
  }
}

Mixed data (dynamic type)

Serialize

If your data contains one or multiple objects of different types, it's possible to define a configuration object instead of the type-string as the first argument of serialize and serializeAsync with these options:

  • type (required): A string for the path to the key to use to determine type or a function deriving a type-string from each data-item.
  • jsonapiObject (optional): Enable/Disable JSON API Object. Default = true.
  • topLevelMeta (optional): Describes the top-level meta. It can be:
    • An object (values can be string or function).
    • A function with one argument function(extraData) { ... } or with two arguments function(data, extraData) { ... }
  • topLevelLinks (optional): Describes the top-level links. It can be:
    • An object (values can be string or function).
    • A function with one argument function(extraData) { ... } or with two arguments function(data, extraData) { ... }

Example :

const typeConfig = {
  // Same as type: 'type'
  type: data => data.type // Can be very complex to determine different types of items.
};

Serializer.serializeAsync(typeConfig, data, { count: 2 }).then(result => {
  // ...
});

Deserialize

If your data contains one or multiple objects of different types, it's possible to define a configuration object instead of the type-string as the first argument of deserialize with these options:

  • type (required): A string for the path to the key to use to determine type or a function deriving a type-string from each data-item.

Example :

const typeConfig = {
  // Same as type: 'type'
  type: data => data.type // Can be very complex to determine different types of items.
};

const deserialized = Serializer.deserializeAsync(typeConfig, data).then(result => {
  // ...
});

Benchmark

Platform info:
==============
Darwin 18.5.0 x64
Node.JS: 10.15.2
V8: 6.8.275.32-node.12
Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz × 8

Suite:
==============
serializeAsync x 64,411 ops/sec ±0.59% (82 runs sampled)
serialize x 150,408 ops/sec ±0.63% (93 runs sampled)
deserializeAsync x 111,881 ops/sec ±0.31% (84 runs sampled)
deserialize x 445,711 ops/sec ±0.29% (94 runs sampled)
serializeError x 294,273 ops/sec ±0.79% (90 runs sampled)
serializeError with a JSON API error object x 16,864,514 ops/sec ±0.32% (93 runs sampled)

License

MIT