Javascript implementation of JSON Predicate (Snell) spec
Switch branches/tags
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
lib
test
.gitignore
.travis.yml
CHANGES.md
LICENSE
README.md
package.json

README.md

json-predicate

npm version Build Status Dependencies Dev Dependencies

Version History

Check/Test/Validate if a block of JSON meets criteria defined by another block of JSON. This is a javascript implementation of the JSON Predicate (Snell) spec.

One likely use of this would be for passing data validations from the back-end to the front-end in a programmatic way. If your back-end is validating incoming data on an API, then it may be useful to send those same validations to the front-end so forms can be checked proactively for the user. The JSON Predicate spec defines the interchange, and this library takes care of reading those validations on the front-end. Now you just need a way to have your back-end serialize out your validations in the JSON predicate format. (Good luck with that!)

Please note that this library does not deal directly with JSON strings, themselves. Instead, it is assumed that the JSON has already been parsed into regular Javascript objects/arrays/expressions.

Given that this library deals with Javascript data (and not the actual JSON strings), it adds a few extra capabilities when it comes to type checking and regular expression matching.

Installation

node:

$ npm install json-predicate

Usage

var jsonPredicate = require('json-predicate');
var jsonTest = jsonPredicate.test;

var passesPredicate = jsonTest(inputData, predicate);
// --> True/False

Operations

contains

Check if the string at path includes the provided substring. Add ignore_case: true to make the check case-insensitive.

var data = {
  a: {
    b: 'Smart People on Ice!'
  }
};

var predicate = {
  op: 'contains',
  path: '/a/b',
  value: 'People'
}

jsonTest(data, predicate); // true

predicate = {
  op: 'contains',
  path: '/a/b',
  value: 'sMaRt PeOpLe',
  ignore_case: true
};

jsonTest(data, predicate); // true

defined

Check if the key at path exists (is not undefined).

var data = {
  a: {
    b: 'You\'ve seen him too?',
    c: null
  }
};

var predicate = {
  op: 'defined',
  path: '/a/b'
}
jsonTest(data, predicate); // true

var predicate = {
  op: 'defined',
  path: '/a/c'
}
jsonTest(data, predicate); // true

predicate = {
  op: 'defined',
  path: '/a/z'
}
jsonTest(data, predicate); // false

ends

Check if the string at path ends with the provided substring. Add ignore_case: true to make the check case-insensitive.

var data = {
  a: {
    b: 'Smart People on Ice!'
  }
};

var predicate = {
  op: 'ends',
  path: '/a/b',
  value: ' Ice!'
}

jsonTest(data, predicate); // true

predicate = {
  op: 'ends',
  path: '/a/b',
  value: 'On ICE!',
  ignore_case: true
};

jsonTest(data, predicate); // true

in

Check if the value at path is included in the provided value array. Compares objects deeply (using lodash _.isEqual under the hood). ignore_case:true can be passed to allow mismatched strings.

  var data = {
    firstName: 'Mitch',
    lastName: 'Taylor'
  }

  var predicate = {
    op: 'in',
    path: '/firstName',
    value: ['chris', 'mitch', 'kent', 'jordan'],
    ignore_case: true
  }

  jsonTest(data, predicate); // true

  var data = {
    title: 'Everything',
    host: {
      firstName: 'Jerry',
      lastName: 'Hathaway'
    }
  };

  var predicate = {
    op: 'in',
    path: '/host',
    value: [
      {firstName: 'Jerry', lastName: 'Hathaway'},
      {firstName: 'Albert', lastName: 'Einstein'}
    ]
  }

  jsonTest(data, predicate); // true

less

Check if the numeric value at path is less than the provided value. Returns false if either the value at path or the predicate value is non-numeric.

  var data = {
    a: 1984
  }

  var predicate = {
    op: 'less',
    path: '/a',
    value: 1985
  }

  jsonTest(data, predicate); // true

matches

Check if the string value at path satisfies the provided regex. The predicate value can be given directly as a regex, or as a string to be turned into a regex. ignore_case:true can also be added to the predicate to make the regex test case-insensitive.

  var data = {
    laser: {
      e: '1x10^6J/l' // "... That's hotter than the sun!"
    }
  }

  var predicate = {
    op: 'matches',
    path: '/laser/e',
    value: '[1-9x^]*J\/l' // pass regex as string
  }

  jsonTest(data, predicate); // true

  var predicate = {
    op: 'matches',
    path: '/laser/e',
    value: /[1-9x^]*J\/l/ // or pass regex directly
  }

  jsonTest(data, predicate); // true

more

Check if the numeric value at path is more than the provided value. Returns false if either the value at path or the predicate value is non-numeric.

  var data = {
    a: 1984
  }

  var predicate = {
    op: 'more',
    path: '/a',
    value: 1983
  }

  jsonTest(data, predicate); // true

starts

Check if the string at path starts with the provided substring. Add ignore_case: true to make the check case-insensitive.

var data = {
  a: {
    b: 'Smart People on Ice!'
  }
};

var predicate = {
  op: 'starts',
  path: '/a/b',
  value: 'Smart People'
}

jsonTest(data, predicate); // true

predicate = {
  op: 'starts',
  path: '/a/b',
  value: 'sMaRt',
  ignore_case: true
};

jsonTest(data, predicate); // true

test

Check if the value at path is equal to the provided value. Compares objects deeply (using lodash _.isEqual under the hood). ignore_case:true can be passed to allow mismatched strings.

  // Match strings:
  var data = {
    firstName: 'Mitch',
    lastName: 'Taylor'
  }

  var predicate = {
    op: 'test',
    path: '/firstName',
    value: 'mitch',
    ignore_case: true
  }

  jsonTest(data, predicate); // true

  // Match objects:
  var data = {
    title: 'Everything',
    host: {
      firstName: 'Jerry',
      lastName: 'Hathaway'
    }
  };

  var predicate = {
    op: 'test',
    path: '/host',
    value: {firstName: 'Jerry', lastName: 'Hathaway'}
  }

  jsonTest(data, predicate); // true

  // Match numbers:
  var data = {
    laser: {
      wavelength: 600,
      unit: 'nm'
    }
  }

  var predicate = {
    op: 'test',
    path: '/laser/wavelength',
    value: 600
  }

  jsonTest(data, predicate); // true

type

Check if the key at path is of the type given in the predicate value. Types can be:

  • number
  • string
  • boolean
  • object
  • array
  • null
  • undefined
  • date (A string conforming to RFC3339 'full-date' spec)
  • date-time (A string conforming to RFC3339 'date-time' spec)
  • time (A string conforming to RFC3339 'full-time' spec)
  • lang (A string conforming to RFC4646 'Language-Tag' spec)
  • lang-range (A string conforming to RFC4647 'language-range')
  • iri (A string conforming to RFC3987 'IRI-reference' spec)
  • absolute-iri (A string conforming to RFC3987 'IRI' spec)

Note: date, date-time, time string matching is based on the following regular expressions. They may not exactly match the RFC specs. I invite pull requests or recommendations for libraries to depend on.

  • date: /^\d{4}-\d{2}-\d{2}$/
  • date-time: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?((?:[\+\-]\d{2}:\d{2})|Z)$/
  • time: /^\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?((?:[\+\-]\d{2}:\d{2})|Z)$/

Note: lang and lang-range string matching is based on the following regular expressions. They may not exactly match RFC specs. I invite pull requests or recommendations for libraries to depend on.

  • lang: /^[a-z]{2,3}(?:-[A-Z]{2,3}(?:-[a-zA-Z]{4})?)?$/
  • lang-range: /^\*|[A-Z]{1,8}(?:-[\*A-Za-z0-9]{1,8})?$/

Note: iri and absolute-iri string matching is based on the valid-url library. With validUrl.isUri() being used for 'iri' type, and validUrl.isWebUri() being used for 'absolute-iri'. This is almost certainly not the correct behavior with respect to RFC3987 and the JSON-predicate spec. In invite pull requests or recommendations to improve this.

var data = {
  num: 23,
  str: "little pickles",
  bool: true,
  obj: { firstName: "Lazlo", lastName: "Hollyfeld" },
  arr: ['tracking system', 'large spinning mirror'],
  nil: null,
  d: '1985-08-07',
  dt: '1985-08-07T19:00:00Z',
  t: '19:00:00-05:00',
  l: 'en-US',
  lr: 'CH-*',
  iri: 'https://github.com/MalcolmDwyer/json-predicate#type',
  absIri: 'https://github.com/MalcolmDwyer/json-predicate'
}

jsonTest(data, {op:'type', path:'/num',       value:'number'});       // true
jsonTest(data, {op:'type', path:'/str',       value:'string'});       // true
jsonTest(data, {op:'type', path:'/bool',      value:'boolean'});      // true
jsonTest(data, {op:'type', path:'/obj',       value:'object'});       // true
jsonTest(data, {op:'type', path:'/arr',       value:'array'});        // true
jsonTest(data, {op:'type', path:'/nil',       value:'null'});         // true
jsonTest(data, {op:'type', path:'/not_a_key', value:'undefined'});    // true
jsonTest(data, {op:'type', path:'/d',         value:'date'});         // true
jsonTest(data, {op:'type', path:'/dt',        value:'date-time'});    // true
jsonTest(data, {op:'type', path:'/t',         value:'time'});         // true
jsonTest(data, {op:'type', path:'/l',         value:'lang'});         // true
jsonTest(data, {op:'type', path:'/lr',        value:'lang-range'});   // true
jsonTest(data, {op:'type', path:'/iri',       value:'iri'});          // true
jsonTest(data, {op:'type', path:'/absIri',    value:'absolute-iri'}); // true

undefined

Check if the key at path does not exist (is undefined).

var data = {
  a: {
    b: 'You\'ve seen him too?',
    c: null
  }
};

predicate = {
  op: 'undefined',
  path: '/a/z'
}
jsonTest(data, predicate); // true

var predicate = {
  op: 'undefined',
  path: '/a/b'
}
jsonTest(data, predicate); // false

var predicate = {
  op: 'undefined',
  path: '/a/c'
}
jsonTest(data, predicate); // false

contained

Note: This operation is not included in the Snell json-predicate spec. If you are sharing predicates with other implementations, you should avoid using this op.

Check if the key at path (which must be an array) contains the value given. (Inverse of in, which has the array in the predicate and the single value in the data). Can compare strings, numbers, shallow and deep objects. Operation honors the ignore_case parameter when comparing string values.

var data = {
  a: {
    b: [1983, 1984, 1985]
  }
}

var predicate = {
  op: 'contained',
  path: '/a/b',
  value: 1984
}

jsonTest(data, predicate); // true

intersects

Note: This operation is not included in the Snell json-predicate spec. If you are sharing predicates with other implementations, you should avoid using this op.

Check if the key at path (which must be an array) has any matching values in common with the provided array data (which also must be an array). As long as one or more elements in the two arrays match (numeric, string, shallow or deep object comparisons, etc.), then the test will return true. If either value is not an array, or if no array elements are found in common, then it returns false. Operation honors the ignore_case parameter for string comparisons.

var data = {
  a: {
    b: [1983, 1984, 1985]
  }
}

var predicate = {
  op: 'contained',
  path: '/a/b',
  value: [1984, 1988, 1992]
}

jsonTest(data, predicate); // true

and

Check if two or more sub-predicates are true.

var data = {
  a: {
    b: 'Much further, much faster!',
    c: 'I don\'t know; I found it in one of the labs.'
  }
}

predicate = {
  op: 'and',
  apply: [
    {
      op: 'contains',
      path: '/a/b',
      value: 'faster!'
    },
    {
      op: 'contains',
      path: '/a/c',
      value: 'labs'
    }
  ]
}
jsonTest(data, predicate); // true

Paths can also be defined in layers:

var data = {
  a: {
    b: 'Much further, much faster!',
    c: 'I don\'t know; I found it in one of the labs.'
  }
}

predicate = {
  op: 'and',
  path: '/a',
  apply: [
    {
      op: 'contains',
      path: '/b',
      value: 'faster!'
    },
    {
      op: 'contains',
      path: '/c',
      value: 'labs'
    }
  ]
}
jsonTest(data, predicate); // true

or

Check if any of two or more sub-predicates are true.

var data = {
  a: {
    b: 'Much further, much faster!',
    c: 'I don\'t know; I found it in one of the labs.'
  }
}

predicate = {
  op: 'or',
  apply: [
    {
      op: 'contains',
      path: '/a/b',
      value: 'faster!' // true
    },
    {
      op: 'contains',
      path: '/a/c',
      value: 'Get out, Lightman!' // false
    }
  ]
}
jsonTest(data, predicate); // true

predicate = {
  op: 'or',
  apply: [
    {
      op: 'contains',
      path: '/a/b',
      value: 'Protovision, I have you now!' // false
    },
    {
      op: 'contains',
      path: '/a/c',
      value: 'Get out, Lightman!' // false
    }
  ]
}
jsonTest(data, predicate); // false

Paths can also be defined in layers:

var data = {
  a: {
    b: 'Much further, much faster!',
    c: 'I don\'t know; I found it in one of the labs.'
  }
}

predicate = {
  op: 'or',
  path: '/a',
  apply: [
    {
      op: 'contains',
      path: '/b',
      value: 'faster!' // true
    },
    {
      op: 'contains',
      path: '/c',
      value: 'Mr. Potatohead!' // false
    }
  ]
}
jsonTest(data, predicate); // true

not

Check if two or more sub-predicates are all false. Equivalent to a logical NOR operation.

var data = {
  a: {
    b: 'Much further, much faster!',
    c: 'I don\'t know; I found it in one of the labs.'
  }
}

predicate = {
  op: 'not',
  apply: [
    {
      op: 'contains',
      path: '/a/b',
      value: 'Joshua, what are you doing?' // false
    },
    {
      op: 'contains',
      path: '/a/c',
      value: 'Hi, Lightman!' // false
    }
  ]
}
jsonTest(data, predicate); // true

Paths can also be defined in layers:

var data = {
  a: {
    b: 'Much further, much faster!',
    c: 'I don\'t know; I found it in one of the labs.'
  }
}

predicate = {
  op: 'not',
  path: '/a',
  apply: [
    {
      op: 'contains',
      path: '/b',
      value: 'Joshua, what are you doing?' // false
    },
    {
      op: 'contains',
      path: '/c',
      value: 'Hi, Lightman!' // false
    }
  ]
}
jsonTest(data, predicate); // true