EasyJSONMatcher
This gem is designed to make it easy for you to validate JSON objects in Ruby. It's a lightweight gem primarily designed to be used in testing. If you want something more comprehensive for a production environment I suggest you look at dry-validation.
The interface uses a plain Ruby DSL to make representing expected JSON output in your test suites very straightforward. No need to work in any other language or even create additional files to store your schemas if you don't want to.
version 0.5.0
Installation
gem install easy_json_matcher
or add
gem 'easy_json_matcher'
to your gemfile.
Usage
Note that EasyJSONMatcher
and EJM
are interchangeable.
SchemaGenerator
EJM::SchemaGenerator
is responsible for providing the client
interface. Use it to define your schemas.
To create a new schema for validating against your JSON objects, create a SchemaGenerator object and pass in a block which defines the expected content of your JSON:
expected = EJM::SchemaGenerator.new do
# Validation logic here
end
The new SchemaGenerator
object defines a couple of messages for retrieving the
Validator
object which you'll use to validate your schema. These messages are:
generate_schema
which returns an instance of Node
that you can use to
validate JSON objects by calling node.valid? json
, or register as: :schema_name
which registers the schema with SchemaLibrary
for future use and
also returns the node as above.
Defining your schema
Primitives
The SchemaGenerator
interface provides a series of messages which you can use
to define your schema. The simplest of these define expected primitive values as
follows:
EJM::SchemaGenerator.new do
number key: :number
boolean key: :boolean
string key: :string
value key: :value
object key: :object
end
Which would correctly validate the below JSON string:
"{
'number': 1,
'boolean': true,
'string': 'EasyJSONMatcher',
'value': null,
'object': {}
}"
The #has_object
message only defines a key/value pair where the value is an
arbitrary json object. For defining schemas that include the details of nested
objects see the section below on #has_schema
or the section on
#contains_node
.
As a matter of fact, all of these methods are just macros for NodeGenerator
's
#has_attribute
method. This method has the following signature:
#has_attribute(key:, opts:)
. So, for instance, has_string key: :s
expands to
has_attribute key: :s, opts: [:string]
. The reason this is significant is that
the library processes the values in your candidate objects in steps, each one
defined by a value in the option array where that value is either a predefined
operation or is a proc/lambda with the following signature: ->(value) { ... }
and returns an error message if an error is found. So you can specify any number
of steps you want for processing the value. See the section on Custom
Validations below.
Dates
Although dates are not part of the JSON specification, they are commonly used in
JSON payloads and so a validator is available using the #has_date
method. It
is used as follows:
EJM::SchemaGenerator.new do
json_schema.date key: :date
end
which would validate the following json object:
"{
date: '2016-03-04'
}"
EasyJSONMatcher currently only supports dates in the SQL format, i.e. YYYY-MM-DD. You can use a proc to verify other date formats.
Describing complex objects
SchemaGenerator
messages which begin with #contains_
take blocks which allow
you to describe attributes on complex nested objects and arrays.
Arrays
JSON payloads can contain arrays, which have to be handled differently from
primitive values. A JSON array can contain values of types equal to any or all
of the above primitives. Therefore #contains_array
allows you to define the
type of values that an object should contain by yielding an object to a block
which responds to a series of methods prefixed with
#should_only_contain{type}
:
EJM::SchemaGenerator.new do
contains_array key: :array do |array|
array.elements_should be: [:number]
end
end
Allowing clients to specify that there can be values of different types may be implemented in future releases.
You can also reuse registered schemas in the same manner. This assumes that the
schema has been registered with SchemaLibrary
EJM::SchemaGenerator.new do |array_schema|
array_schema.elements_should be: [:schema_name]
end
Objects
JSON objects can contain nested objects. These can be defined in the schema in
two different ways, either by defining them inline with #contains_node
or
using #has_schema
.
contains_node
#contains_node
yields an object that responds to the same messages as that
yielded by SchemaGenerator#new
, so you can define the content of the expected
object in the same way that you define the content of your top-level
SchemaGenerator
instance. For instance:
EJM::SchemaGenerator.new do
contains_node key: :level_1 do
has_string key: :title
contains_node key: :level_2 do
has_string: key: :sub_heading
end
end
}
...would validate the following json object:
"{
'level_1': {
'title': 'level one',
'level_2': {
'sub_heading': 'level two'
}
}
}"
has_schema
has_schema
allows the user to reuse a schema that has already been registered
with the SchemaLibrary
. The message accepts two arguments, the key and the
name as follows:
EJM::SchemaGenerator.new do
reusable.has_string key: :name
end.register as: :reusable
EJM::SchemaGenerator.new do
schema.has_schema key: :reused_schema, name: :reusable
end
will validate the following json object:
"{
'reused_schema': {
'name': 'Reusable'
}
}"
Plural Requirements
All of the above methods have a corresponding plural method that allows you to reduce boilerplate. For instance the following code snippets are equivalent:
has_string key: :a, [:required]
has_string key: :b, [:required]
has_strings keys: [:a, :b], [:required]
Options
All the messages that define the content of a schema can accept an options hash
with the keyword :opts
, for instance:
has_string key: :string, opts: [:required]
This applies to methods prefixed with both has_
and contains_
.
The options accepted by all validators are as follows:
required:
indicates that the value must be present. Note that has_value will
accept null
as a value in this case, but the key must be present in the JSON
object.
strict:
If this is set to true
then the schema will only validate
a node where its keyset is equal to or a subset of the expected keyset. If it
has any keys in addition to the expected set then the schema will be invalid.
Custom Validations
Sometimes simply knowing that a value is the correct type and is present isn't
enough. Sometimes you need more detail. To facilitate this, the opts
array can
contain a lambda like so:
EJM::SchemaGenerator.new do
has_value key: val, opts: [ ->(candidate){ "value should say hello world" unless value == "hello world" ] }
end
Obviously you can reuse the same lamda in different places. The lambda object must return an object which is the error message that is to be used if the value is not valid.
Occasionally, you want to stop the validation process. For instance, if a value
is nil
, then you want to stop the validation process before it reaches any
other verification steps where nil
is unexpected. If you want to halt the
process and just return the set of errors generated so far, then your lambda
should return the literal false
. Not false as in false-y. False as in
false.is_a? FalseClass
.
Global Options
Options can also be added as defaults for the entire schema by passing a Hash
to the global_defaults:
argument for EJM::SchemaGenerator.new
,
so for instance:
EJM::SchemaGenerator.new(global_opts: [:required])
would generate a validator that required all its keys to be present. Global
settings can be overriden for a specific attribute by setting their values in
the usual way. Note that these settings will not apply to schemas that have been
recovered from SchemaLibrary
.
Validation
Once you have defined your schema, you can retrieve the generated validator
using #generate_schema
. The object that is returned responds to the #valid?
method to which you pass your JSON object. It will return true
iff the object
complies with the schema:
schema = EJM::SchemaGenerator.new do
#define the schema
end.generate_schema
schema.valid? json
Error Messages
In order to assist you to understand why the candidate was considered invalid,
an Array
detailing the reasons why any elements were invalid can be retrieved
by calling #validate(candidate: your_sample)
.
Reusing Schemas
Any schema can be registered with the SchemaLibrary
object, by sending the
#register
message to an instance of SchemaGenerator
. Doing so explicitly
registers the schema's Validator
with SchemaLibrary
and also returns the
generated Validator
. So you can do the following:
EJM::SchemaGenerator.new do
#define the schema
end.register as: :saved_schema
retrieved = EJM::SchemaLibrary.get_schema name: :saved_schema
validity = retrieved.valid? json
You can also add a schema directly using
EJM::SchemaLibrary#add_schema
if you wish.
Issues
If you find any aspect of the gem which does not behave as expected, please raise an issue. If the issue relates specifically to a JSON payload validating incorrectly for a given schema, please supply both the code you used to create the schema and the object that did not validate in your bug report. That will make it much easier to track down the issue!
Contributing
I hope you find this gem useful. Please feel free to create an issue if you would like to suggest an enhancement, and then it would be great if you could implement a patch and submit a pull request too!