node package manager

mod-json

mod-json

mod-json lets you modularize your JSON files with $include and $extend directives. mod-json is a docmodel component which has been split out into its own module. There are synchronous and asynchronous loaders available (the latter returning bluebird promises), all underlying utilities are publicly exposed, and typings are bundled with the module.

Install

npm install mod-json

Usage

In the following example, we have 2 JSON files (one.json, and two.json). one.json contains a key that's value will be the contents of two.json. Additionally, there is an object which builds on ($extends) another object.

one.json

{
  "a": 1, 
  "b": {"$include": "two.json"},
  "c": {
    "$extend": "b",
    "e"      : 4
  }
}

two.json

{
  "d": 2, 
  "e": 3
}

Module Usage

// Require module. TS: import * as json from 'mod-json' 
const json = require('mod-json');
 
// Asynchronous [promise] 
json.load('/path/to/one.json')
.then(object => {
  // ... 
})
.catch(err => {
  // ... 
})
 
// Synchronous 
const object = json.load_sync('/path/to/one.json')
 

The result of object in both cases will be:

{
  "a": 1, 
  "b": {
    "d": 2, 
    "e": 3
  },
  "c": {
    "d": 2,
    "e": 4
  }
}

JSON $include

An object where the only key is $include and the value is a path to the JSON file you would like to include.

.json at the end of paths is optional. Both of the following includes are identical:

{
  "a": {"$include": "some_other_file.json"},
  "b": {"$include": "some_other_file"}
}

$includes can be nested to any level, and are processed depth-first. Please note that there is currently no explicit protection against a file including itself infinitely. This will be, em, include this is the next version.

File paths are resolved relative to an include directory. In default configuration, this will be the directory of the initial file you pass to a load() or load_sync() call. See API descriptors, below, for more details.

$include directives are resolved before $extend directives.

JSON $extend

An object where one of its keys is $extend and the value is the key of an object you would like to inherit properties from. The extended object can add new keys, or override values inherited from the base:

{
  "a": {
    "b": 1, 
    "c": 2
  },
  "b": {
    "$extend": "a",
    "c"      : 3,
    "d"      : 4
  }
}

$extends can be nested to any level and are processed depth-first. If the requested base cannot be found, the extended object will be preserved with its remaining values. An object cannot extend itself.

$extend directives are resolved after $include directives.

API

load()

load(path: string, options?: {cwd?: string, ignore_bad_files: boolean = false}): promise<any>
  • Load a JSON file and process/resolve all $include and $extend directives within.
  • Returns a promise for the resolved object.
  • Throws if any file cannot be opened or parsed. To override this behavior and silently ignore bad files, set the options ignore_bad_files value to true. In this case, any $includes which cannot be resolved will be set to null.

Please note that file paths are resolved relative to an include directory, not relative to the containing JSON file. If the cwd option is set, this will be used as the include directory, and paths will be resolve relative to it. If cwd is not set, it will be inferred from the path you pass to load(), i.e. all files will be loaded relative to the initial path's directory. In both cases, all files (including the initial file passed to load()) are included relative to the cwd location (whether it is explicitly set or inferred).

const json = require('mod-json');
const path = require('path');
 
const file = path.resolve(__dirname, './includes-directory/some_file.json');
 
json.load(file)
.then(object => {
  // ... 
})
.catch(err => {
  // ... 
});
 
// Alternative for the same result: 
json.load('some_file.json', {cwd: path.resolve(__dirname, './includes-directory/')})
.then(object => {
  // ... 
})
.catch(err => {
  // ... 
});
 
// Silently ignore bad files: 
json.load(file, {ignore_bad_files: true})
.then(object => {
  // object will be null if root file could not 
  // be opened / parsed.  
});
 

load_sync()

load_sync(path: string, options?: {cwd?: string}): any
  • Load a JSON file and process/resolve all $include and $extend directives within.
  • Returns the resolved object.
  • If any file cannot be opened / parsed, will return null.

Please note that file paths are resolved relative to an include directory, not relative to the containing JSON file. If the cwd option is set, this will be used as the include directory, and paths will be resolve relative to it. If cwd is not set, it will be inferred from the path you pass to load_sync(), i.e. all files will be loaded relative to the initial path's directory. In both cases, all files (including the initial file passed to load_sync()) are included relative to the cwd location (whether it is explicitly set or inferred).

const json = require('mod-json');
const path = require('path');
 
const file   = path.resolve(__dirname, './includes-directory/some_file.json');
const object = json.load_sync(file);
 
// Alternative for the same result: 
// const object = json.load_sync('some_file.json', {cwd: path.resolve(__dirname, './includes-directory/')}); 
 

Other Functions

While mod-json is mainly intended as a single function (in sync and async variants) module, all of its underlying functions are also publicly exposed. As such, these are also described here.

resolve_directive_include()

resolve_directive_include(object: any, options: {cwd: string, ignore_bad_files: boolean}): promise<any>
  • Underlying functionality for processing asynchronous includes in an object.
  • There is no option to specify a file path to load an initial / root file--this function only works on existing objects. However, you can pass an on-the-fly object literal which $includes the desired initial file. This is how load() makes use of this function. E.g.:
const json = require('mod-json');
const path = require('path');
 
const cwd = path.resolve(__dirname, './includes-directory/');
 
json.resolve_directive_include({$include: 'some_file.json'}, {cwd: cwd, ignore_bad_files: false})
.then(object => {
  // etc. 
})

Note that you must explicitly pass option values to this function. If an object containing an $include directive has any siblings, these will be removed (the object is replaced with the contents of the included file).

resolve_directive_include_sync()

resolve_directive_include_sync(object: any, options: {cwd: string}): any
  • Underlying functionality for processing synchronous includes in an object.
  • There is no option to specify a file path to load an initial / root file--this function only works on existing objects. However, you can pass an on-the-fly object literal which $includes the desired initial file. This is how load_sync() makes use of this function. E.g.:
const json = require('mod-json');
const path = require('path');
 
const cwd    = path.resolve(__dirname, './includes-directory/');
const object = json.resolve_directive_include_sync({$include: 'some_file.json'}, {cwd: cwd});

Note that you must explicitly pass option values to this function. If an object containing an $include directive has any siblings, these will be removed (the object is replaced with the contents of the included file).

resolve_directive_extend()

resolve_directive_extend(object: any, original?: any): any
  • Underlying functionality to process extends. This is always a synchronous operation.
  • The optional original parameter is used by the function to track the original object for key searches, as the function is recursively called. However, you could pass another object here to extend values from it, rather than the object which contains the $extend directives.
  • Uses utils.deep_find() to search for the base object--this is a depth-first search.
const json = require('mod-json');
 
let an_object = {
  a {
    b: 1
  },
  b {
    c: 2
  },
  d: {
    $extend: b
  }
}
 
// d will extend a.b (first dept-first occurrence of 'b') 
let resolved = json.resolve_directive_extend(an_object);
 

utils.is_object()

is_object(value: any): boolean
  • Returns false for strings, numbers, arrays, null, NaN, undefined, etc. True for all other types.
const json = require('mod-json');
 
json.utils.is_object('foo');        // false 
json.utils.is_object(null);         // false 
json.utils.is_object(undefined);    // false 
json.utils.is_object(42);           // false 
json.utils.is_object([1, 2, 3]);    // false 
 
json.utils.is_object({});           // true 
json.utils.is_object(new Object()); // true 
json.utils.is_object({foo: 'bar'}); // true 
 

utils.merge()

merge(...objects: any[]): any
  • Merge 1 (copy) to n objects into a new object. Values from right-most objects will always take precedence.
  • Copies are semi-shallow. merge() will dig as deep as it can, but only simple values (strings, numbers, booleans etc.) are copied; all other values will reference their original objects.
const json = require('mod-json');
 
// {a: 4, b: 2, c: {d: 3, e: 5}} 
let merged = json.utils.merge(
  {a: 1, b: 2, c: {d: 3}}, 
  {a: 4, c: {e: 5}}
); 

utils.objects_are_equal()

objects_are_equal(a: any, b: any): boolean
const json = require('mod-json');
 
// True 
json.utils.objects_are_equal(
  {a: 1, b: {c: 2}}, 
  {a: 1, b: {c: 2}}
);
  • Performs a recursive deep-equality check of the values in 2 objects.
  • Internal items are checked with strict equality operators (===)
  • Will also return true for: simple values which return true for a strict equality check, the same object as both a and b

utils.deep_find()

deep_find(object: any, key: any): any
  • Performs a depth-first search in object for the given key.
  • Returns the value of the first key match, or null if the key is not found.
  • Keys can be of any type which is comparable with strict equality operators (===).
const json = require('mod-json');
 
let object_to_search = {
  a: {
    b: 1
  },
  b: 2,
  c: {
    b: 3
  }
};
 
json.utils.deep_find(object_to_search, 'b'); // 1 
json.utils.deep_find(object_to_search[c], 'b'); // 3 
json.utils.deep_find(object_to_search, 'e'); // null 

Test

npm test

Typings

TypeScript definition files are bundled with the module and will be available out-of-type box (assuming TypeScript 2 or newer).

Contribute

Contributions in the form of code are most welcome through https://bitbucket.org/ShaneGavin/mod-json

License

Copyright (c) 2017, Shane Gavin (nodehead.com)

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.