A generic JSON schema-to-schema object mapping library.
Object transformation in one form or another is a nearly-universal requirement. Applications containing a substantial variety of transformations may benefit from expressing them declaratively. And applications managing many differing object structures are likely to benefit from schema validation. Declarative transformations in such applications are naturally expressed as a mapping between schemas.
This library implements just such an approach, minimally and generically, offered as demonstration of promise and proof of viability.
Given an input JSON object with a schema describing it, an output schema, and a definition object mapping between them:
const input = {
foo: 'bar'
}
const inputSchema = {
properties: {
foo: { type: 'string' }
}
}
const outputSchema = {
properties: {
contents: {
type: 'object',
properties: {
value: { type: 'string' },
objectType: { type: 'string' }
}
}
}
}
const definition = {
contents: {
$nested: {
$constant: { objectType: 'example' },
value: {
source: 'foo',
lookup: { bar: 'baz' }
}
}
}
}
We can trivially compile and apply a mapper:
const Mapper = require('object-mapping')
const mapper = new Mapper()
mapper.compile(definition, inputSchema, outputSchema)
mapper.applyTo(input)
Producing our desired output:
{
contents: {
value: { 'baz' },
objectType: 'example' }
}
}
A mapping definition object is a template for constructing a JSON object from a single, flat source object*. The keys of the definition are the keys of the output object, while the values of the definition are strategies for retrieving or generating the values of the output object.
* flat may change in the future with the introduction of JSON path expressions, but single is unlikely to change.
There are five strategies for retrieving values:
The foundational strategy is source, which specifies a key to be looked up in the source object. Intuitively, this strategy simply copies values from the source to the output object, potentially under a different key.
This example definition
{
"myTargetProperty": { "source": "mySourceProperty" }
}
will transform { "mySourceProperty": "foo" } into
{ "myTargetProperty": "foo" }.
Next we have the lookup strategy, which embeds a lookup table in the mapping definition itself. Lookup requires the presence of a source strategy; the value retrieved via the source strategy is looked-up in the keys of the embedded lookup table.
This example definition
{
"myTargetProperty": {
"source": "mySourceProperty"
"lookup": {
"foo": "bar"
}
}
}
will transform { "mySourceProperty": "foo" } into
{ "myTargetProperty": "bar" }.
Alternatively, the value of the lookup strategy can be a string referencing
a named lookup table, to be resolved by the lookupResolver passed to the
Mapper constructor (see api below).
Notes
- Default values can be supported by using the empty string as a key in the lookup table.
- An Error is thrown if a value cannot be found in the lookup table.
Next we have the function strategy, which can be used in two modes. In the first, a function is specified in conjunction with a source; as one might expect, the referenced function is applied to the value retrieved via the source strategy. In the second mode, function alone is specified. In this case, the referenced function is applied to the entire source object. This mode therefore supports compound values, complex conditionals, etc.
Functions a referenced by name and resolved by the functionResolver passed
to the Mapper constructor (see api below).
This example definition
{
"myTargetProperty": {
"source": "mySourceProperty",
"function": "upperCase"
}
}
will transform { "mySourceProperty": "foo" } into
{ "myTargetProperty": "FOO" } (assuming the presence of an upperCase
function).
And this example definition
{
"myTargetProperty": {
"function": "JSON.stringify"
}
}
will transform { "mySourceProperty": "foo" } into
{ "myTargetProperty": "{\\"mySourceProperty\\":\\"foo\\"}" } (assuming the
availability of JSON.stringify).
Like function, the $nested strategy can operate in two modes: the first results in a nested sub-object, the second results in a nested array of objects.
This example definition
{
"myTargetProperty": {
"$nested": {
"myNestedProperty": {
"source": "mySourceProperty"
}
}
}
}
will transform { "mySourceProperty": "foo" } into
{ "myTargetProperty": { "myNestedProperty": foo" } }.
And this example definition
{
"myTargetProperty": {
"$nested": [{
"myNestedProperty": {
"source": "mySourceProperty"
}
}, {
"myNestedProperty": {
"source": "anotherProperty"
}
}]
}
}
will transform { "mySourceProperty": "foo", "anotherProperty": "bar" } into
{ "myTargetProperty": [{ "myNestedProperty": foo" }, { "myNestedProperty": bar" }] }.
Note: for nested arrays where only one object is being mapped, $nested does not have to be an array itself; the output object will automatically be promoted to an array based on the schema type of target property.
And finally, as a convenience, constant properties can be defined in the mapping itself.
This example definition
"$constant": {
"myFirstConstant": "#1",
"mySecondConstant": "#2"
},
"myTargetProperty": {
"source": "mySourceProperty"
}
will transform { "mySourceProperty": "foo" } into
{ "myTargetProperty": "foo", "myFirstConstant": "#1", "mySecondConstant": "#2" }
This library exports a simpler Mapper class. The constructor accepts a
config object with the following properties:
origin: a string (orundefined) for annoting the origin of nested objects within arrays (default:'mapper')functionResolver: a function fromstringtoFunctionfor looking up function references (default throwsError)lookupResolver: a function fromstringtoFunctionfor looking up references to named lookup-tables (default throwsError)
An instance of Mapper has the following public methods:
Validates and compiles a definition object mapping from inputSchema to
outputSchema into an optimized internal representation, which is saved to
this.mapping.
Applies the compiled mapping to the supplied data, returning the output object.