An initializer function is itself a function of a JSON Schema. Initializer functions are generated by parsing a schema and generating code to perform object initialization based on default schema values, etc. The purpose of generating functions rather than traversing the schema during initialization is performance. Code without iteration and repetitive heap access is orders of magnitude faster.
A generated function might conform to a set of predetermined behaviors, or it might take options. There are many ways the code for initializer functions could be expressed.
There are two options that need to be considered in the initialization process.
defaults
: This option specifies whether default
values should be included in the resultant object (where values are not
specified on the source). This defaults to true.
filter
: When filter is true, the source object is
not being deep copied onto the target. Rather, the value of the source
object -- of any field that is defined on the schema as it is being
traversed -- is copied to the target object. Thus, data that is
specified by the schema (regardless of correctness) is copied and fields
that are not specified by the schema are ignored. This defaults to
false.
These two options in any combination produce the following behaviour:
defaults | filter additional | behavior |
---|---|---|
0 | 1 | iterate over schema and assign, without initializing defaults |
0 | 0 | deep copy |
1 | 1 | iterate over schema and assign, generating defaults |
1 | 0 | deep copy, then iterate over schema and assign, generating defaults |
In order to rigorously examine and evaluate the various forms generated code might take, we shall herein build up from the simplest possible cases.
Greg says:
When 'additional' is false, we're not deep copying the source object to the target, but we're not validating the data either. Thus, incorrect data (according the schema) is allowed, but data that is not present on the schema at all is disallowed.
"Deep copy for all fields that are defined on the schema"
Given the following schema:
let schema = {
type: 'object',
properties: {
a: {
type: 'object',
properties: {
b: {
type: 'number'
}
},
}c: {
type: 'boolean'
}
} }
The most naive initializer function implementation performs direct referencing for assignment.
function (target, source) {
.a.b = source.a.b
target.c = source.c
target }
There are two immediate problems with the foregoing. First, we don't
want to assign undefined
to the target if the property is
not a member of the source object. Second, In any case where
target.a
or source.a
is not defined an error
will be thrown.
We only want to set properties on the target object if they are
present on the source. We also only want to create the nested container
object target.a
if source.a
is defined.
Let's deal with the latter problem first.
function (target, source) {
if (source.a && !target.a) {
.a = {}
target
}
.a.b = source.a.b
target.c = source.c
target }
And now let's not assign undefined properties of the source object to undefined properties of the target.
function (target, source) {
if (source.a && !target.a) {
.a = {}
target
}
if (source.a && source.a.b) {
.a.b = source.a.b
target
}
if (source.c) {
.c = source.c
target
} }
This code suffers from the truthiness qualities of JavaScript
primitives. In the case where a member of the source object is present
with the value null
, false
, ""
,
or undefined
, the corresponding member will not be set on
the target object.
Let's fix that. While we're at it, let's use bracket references to accommodate property names that are not valid JavaScript symbols.
function (target, source) {
if (source.hasOwnProperty('a') && !target.hasOwnProperty('a')) {
'a'] = {}
target[
}
if (source.hasOwnProperty('a') && source.a.hasOwnProperty('b')) {
'a']['b'] = source['a']['b']
target[
}
if (source.hasOwnProperty('c')) {
'c'] = source['c']
target[
} }
Our next problem is repetitive access to values which live on the
heap. Introducing new variables for the a
object on both
source and target let's us manipulate that object directly without
reference to it's parent.
function (target, source) {
if (source.hasOwnProperty('a') && !target.hasOwnProperty('a')) {
'a'] = {}
target[
}
let sc = source['a']
let tc = target['a']
if (sc && sc.hasOwnProperty('b')) {
'b'] = sc['b']
tc[
}
if (source.hasOwnProperty('c')) {
'c'] = source['c']
target[
} }
This may have a negligible impact with one member to assign, but with
a large number of properties on a
the difference becomes
meaningful. Let's extend our schema to illustrate.
let schema = {
type: 'object',
properties: {
a: {
type: 'object',
properties: {
b: { type: 'number' },
c: { type: 'string' },
d: { type: 'boolean' },
e: { type: 'array' }
},
}f: {
type: 'boolean'
}
} }
And the initializer function:
function (target, source) {
if (source.hasOwnProperty('a') && !target.hasOwnProperty('a')) {
'a'] = {}
target[
}
let sc = source['a']
let tc = target['a']
if (sc) {
if (sc.hasOwnProperty('b')) {
'b'] = sc['b']
tc[
}
if (sc.hasOwnProperty('c')) {
'c'] = sc['c']
tc[
}
if (sc.hasOwnProperty('d')) {
'd'] = sc['d']
tc[
}
if (sc.hasOwnProperty('e')) {
'e'] = sc['e']
tc[
}
}
if (source.hasOwnProperty('f')) {
'f'] = source['f']
target[
} }
What happens when we have more than one nested schema at the root level
let schema = {
type: 'object',
properties: {
a: {
type: 'object',
properties: {
b: { type: 'number' },
c: { type: 'string' }
},
}d: {
type: 'object',
properties: {
e: { type: 'boolean' },
f: { type: 'array' }
},
}g: {
type: 'boolean'
}
} }
We can avoid creating additional variables on the stack by reusing
the symbols sc
and tc
(defined for
a
in the previous example) with d
.
function (target, source) {
var sc
var tc
if (source.hasOwnProperty('a') && !target.hasOwnProperty('a')) {
'a'] = {}
target[
}
= source['a']
sc = target['a']
tc
if (typeof sc === 'object') {
if (sc.hasOwnProperty('b')) {
'b'] = sc['b']
tc[
}
if (sc.hasOwnProperty('c')) {
'c'] = sc['c']
tc[
}
}
if (source.hasOwnProperty('d') && !target.hasOwnProperty('d')) {
'd'] = {}
target[
}
= source['d']
sc = target['d']
tc
if (typeof sc === 'object') {
if (sc.hasOwnProperty('e')) {
'e'] = sc['e']
tc[
}
if (sc.hasOwnProperty('f')) {
'f'] = sc['f']
tc[
}
}
if (source.hasOwnProperty('g')) {
'g'] = source['g']
target[
} }
NOTE: in creating nested containers on target, we'll eventually need to consider the case of Array containers.
What happens when we have more than one level of nesting.
let schema = {
type: 'object',
properties: {
a: {
type: 'object',
properties: {
b: {
type: 'object',
properties: {
c: { type: 'boolean' },
d: { type: 'array' }
},
}e: { type: 'number' }
},
}f: {
type: 'object',
properties: {
g: { type: 'boolean' }
}
}
} }
As we traverse this schema, each time we nest deeper, we need to
create new symbols bound to the parent of properties at that new level.
Without doing so, we will lose track of the grandparent
(a
), which we'll need in order to reference any subsequent
siblings (e
) of the parent (b
).
We can reuse symbols as long as they're being resused for the same depth in the nested schema.
function (target, source) {
var source1
var target1
var source2
var target2
if (source.hasOwnProperty('a') && !target.hasOwnProperty('a')) {
'a'] = {}
target[
}
= source['a']
source1 = target['a']
target1
if (typeof source1 === 'object') {
if (source1.hasOwnProperty('b') && !target1.hasOwnProperty('b')) {
'b'] = {}
target1[
}
// we can't reuse source1/target1 here because we'll need a reference
// to it's current value when moving on from `b`.
= source1['b']
source2 = target1['b']
target2
if (typeof source2 === 'object') {
if (source2.hasOwnProperty('c')) {
'c'] = source2['c']
target2[
}
if (source2.hasOwnProperty('d')) {
'd'] = source2['d']
target2[
}
}
if (source1.hasOwnProperty('e')) {
'e'] = source1['e']
target1[
}
}
if (source.hasOwnProperty('f') && !target.hasOwnProperty('f')) {
'f'] = {}
target[
}
= source['f']
source1 = target['f']
target1
if (typeof source1 === 'object') {
if (source1.hasOwnProperty('g')) {
'g'] = source1['g']
target1[
}
} }
We now address the problems of source values that differ from the
expectation of the schema. If a source object contains a value such as
false
where a nested object is expected, we should copy
that value, instead of trying to traverse deeper into the source.
We also want to avoid overwriting existing values on the target if there is no value defined on the source and a non-object value defined for the same property on the target.
Further, we want to ensure that if the source member is an object, that the target becomes an object, regardless of what may or may not be defined there.
function (target, source) {
var source1
var target1
var source2
var target2
// CHANGED FROM PREVIOUS
if (source.hasOwnProperty('a')) {
if (typeof source['a'] === 'object') {
if (!target.hasOwnProperty('a') || typeof target['a'] !== 'object') {
'a'] = {}
target[
}else {
} 'a'] = source['a']
target[
}
}
= source['a']
source1 = target['a']
target1
if (typeof source1 === 'object') {
if (source1.hasOwnProperty('b')) {
'b'] = source1['b']
target1[
}
if (source1.hasOwnProperty('c')) {
if (typeof source1['c'] === 'object') {
if (!target1.hasOwnProperty('c') || typeof target['c'] !== 'object') {
'c'] = {}
target1[
}else {
} 'c'] = source1['c']
target1[
}
}
= source1['c']
source2 = target1['c']
target2
if (typeof source2 === 'object') {
if (source2.hasOwnProperty('d')) {
'd'] = source2['d']
target2[
}
if (source2.hasOwnProperty('e')) {
'e'] = source2['e']
target2[
}
}
}
if (source.hasOwnProperty('f')) {
if (typeof source['f'] === 'object') {
if (!target.hasOwnProperty('f') || typeof target['f'] !== 'object') {
'f'] = {}
target[
}else {
} 'f'] = source['f']
target[
}
}
= source['f']
source1 = target['f']
target1
if (typeof source1 === 'object') {
if (source1.hasOwnProperty('g')) {
'g'] = source1['g']
target1[
}
} }
In the preceding example, we're still prematurely creating nested container objects on the target where there may be no corresponding values defined in the source object. In order to avoid this, we'll need to delay assigning the container object until we know there are properties assigned to it. This requires keeping a counter of properties assigned and checking that value upon completion of the branch.
Once again, count symbols can be reused. However, we'll need a new counter for each level of nesting.
function (target, source) {
var source1
var target1
var count1
var source2
var target2
var count2
if (source.hasOwnProperty('a')) {
if (typeof source['a'] === 'object') {
if (!target.hasOwnProperty('a') || typeof target['a'] !== 'object') {
= {}
target1
}else {
} 'a'] = source['a']
target[
}
}
= source['a']
source1 = 0
count1
if (typeof source1 === 'object') {
if (source1.hasOwnProperty('b')) {
'b'] = source1['b']
target1[++
count1
}
if (source1.hasOwnProperty('c')) {
if (typeof source1['c'] === 'object') {
if (!target1.hasOwnProperty('c') || typeof target['c'] !== 'object') {
= {}
target2
}else {
} 'c'] = source1['c']
target1[++
count1
}
}
= source1['c']
source2 = 0
count2
if (typeof source2 === 'object') {
if (source2.hasOwnProperty('d')) {
'd'] = source2['d']
target2[++
count2
}
if (source2.hasOwnProperty('e')) {
'e'] = source2['e']
target2[++
count2
}
}
if (count2 > 0) {
'c'] = target2
target1[++
count1
}
}
if (count1 > 0) {
'a'] = target1
target[
}
if (source.hasOwnProperty('f')) {
if (typeof source['f'] === 'object') {
if (!target.hasOwnProperty('f') || typeof target['f'] !== 'object') {
= {}
target1
}else {
} 'f'] = source['f']
target[
}
}
= source['f']
source1 = 0
count1
if (typeof source1 === 'object') {
if (source1.hasOwnProperty('g')) {
'g'] = source1['g']
target1[++
count1
}
}
if (count1 > 0) {
'f'] = target1
target[
} }
We're potentially performing a superfluous variable assignment in the above code along with duplicating conditional logic.
if (source.hasOwnProperty('a')) {
if (typeof source['a'] === 'object') {
if (!target.hasOwnProperty('a') || typeof target['a'] !== 'object') {
= {}
target1
}else {
} 'a'] = source['a']
target[
}
}
// this is unnecessary if `source.hasOwnProperty('a')` is false
= source['a']
source1 = 0
count1
// this check is being duplicated and is not necessary when `source.hasOwnProperty('a')` is false
if (typeof source1 === 'object') {
if (source1.hasOwnProperty('b')) {
This issue is addressed by refactoring the potentially unnecessary code to happen only if the relevant conditional passes.
function (target, source) {
var source1
var target1
var count1
var source2
var target2
var count2
if (source.hasOwnProperty('a')) {
if (typeof source['a'] === 'object') {
if (!target.hasOwnProperty('a') || typeof target['a'] !== 'object') {
= {}
target1 else {
} = target['a']
target1
}
= source['a']
source1 = 0
count1
if (source1.hasOwnProperty('b')) {
'b'] = source1['b']
target1[++
count1
}
if (source1.hasOwnProperty('c')) {
if (typeof source1['c'] === 'object') {
if (!target1.hasOwnProperty('c') || typeof target['c'] !== 'object') {
= {}
target2 else {
} = target['a']['c']
target2
}
= source1['c']
source2 = 0
count2
if (source2.hasOwnProperty('d')) {
'd'] = source2['d']
target2[++
count2
}
if (source2.hasOwnProperty('e')) {
'e'] = source2['e']
target2[++
count2
}
if (count2 > 0) {
'c'] = target2
target1[++
count1
}
else {
} 'c'] = source1['c']
target1[++
count1
}
}
if (count1 > 0) {
'a'] = target1
target[
}
else {
} 'a'] = source['a']
target[
}
}
if (source.hasOwnProperty('f')) {
if (typeof source['f'] === 'object') {
if (!target.hasOwnProperty('f') || typeof target['f'] !== 'object') {
= {}
target1 else {
} = target['f']
target1
}
= source['f']
source1 = 0
count1
if (source1.hasOwnProperty('g')) {
'g'] = source1['g']
target1[++
count1
}
if (count1 > 0) {
'f'] = target1
target[
}
else {
} 'f'] = source['f']
target[
}
} }
if you want to allow additional attributes and don't need to generate defaults, the most efficient initialization is deep copy.
function initialize1 (target, source, options) {
return Object.assign(target, JSON.parse(JSON.stringify(source)))
}
Simplest possible case.
let schema = {
type: 'object',
properties: {
a: { default: 'foo' }
} }
function (target, source) {
'a'] = source['a'] || 'foo'
target[ }
function (target, source) {
if (source.hasOwnProperty('a')) {
'a'] = source['a']
target[else {
} 'a'] = 'foo'
target[
} }
let schema = {
type: 'object',
properties: {
a: {
type: 'object',
properties: {
b: { type: 'number' },
c: {
type: 'object',
properties: {
d: { type: 'boolean', default: false },
e: { type: 'string' }
}
}
},
}f: {
type: 'object',
properties: {
g: { type: 'boolean' }
}
}
} }
Optional defaults
function (target, source) {
var source1
var target1
var count1
var source2
var target2
var count2
if (source.hasOwnProperty('a')) {
if (typeof source['a'] === 'object') {
if (!target.hasOwnProperty('a') || typeof target['a'] !== 'object') {
= {}
target1 else {
} = target['a']
target1
}
= source['a']
source1 = 0
count1
if (source1.hasOwnProperty('b')) {
'b'] = source1['b']
target1[++
count1
}
if (source1.hasOwnProperty('c')) {
if (typeof source1['c'] === 'object') {
if (!target1.hasOwnProperty('c') || typeof target['c'] !== 'object') {
= {}
target2 else {
} = target['a']['c']
target2
}
= source1['c']
source2 = 0
count2
if (source2.hasOwnProperty('d')) {
'd'] = source2['d']
target2[++
count2
// CHANGED TO ADD DEFAULT
else if (options.defaults !== false) {
} 'd'] = false
target2[++
count2
}
if (source2.hasOwnProperty('e')) {
'e'] = source2['e']
target2[++
count2
}
if (count2 > 0) {
'c'] = target2
target1[++
count1
}
else {
} 'c'] = source1['c']
target1[++
count1
}
}
if (count1 > 0) {
'a'] = target1
target[
}
else {
} 'a'] = source['a']
target[
}
}
if (source.hasOwnProperty('f')) {
if (typeof source['f'] === 'object') {
if (!target.hasOwnProperty('f') || typeof target['f'] !== 'object') {
= {}
target1 else {
} = target['f']
target1
}
= source['f']
source1 = 0
count1
if (source1.hasOwnProperty('g')) {
'g'] = source1['g']
target1[++
count1
}
if (count1 > 0) {
'f'] = target1
target[
}
else {
} 'f'] = source['f']
target[
}
} }
let schema = {
type: 'object',
properties: {
a: {
type: 'object',
properties: {
b: { type: 'number' },
c: {
type: 'object',
properties: {
d: {
type: 'array',
items: {
e: { type: 'boolean', default: false },
f: { type: 'string' }
},
}
}
}
}
}
}
}
let example = {
a: {
b: 3,
c: {
d: [
e: true, f: 'foo' },
{ e: false, f: 'bar' },
{ e: true, f: 'baz' },
{ e: false, f: 'qux' },
{
]
}
} }
function (target, source) {
var source1
var target1
var count1
var source2
var target2
var count2
if (source.hasOwnProperty('a')) {
if (typeof source['a'] === 'object') {
if (!target.hasOwnProperty('a') || typeof target['a'] !== 'object') {
= {}
target1 else {
} = target['a']
target1
}
= source['a']
source1 = 0
count1
if (source1.hasOwnProperty('b')) {
'b'] = source1['b']
target1[++
count1
}
if (source1.hasOwnProperty('c')) {
if (typeof source1['c'] === 'object') {
if (!target1.hasOwnProperty('c') || typeof target1['c'] !== 'object') {
= {}
target2 else {
} = target1['c']
target2
}
= source1['c']
source2 = 0
count2
if (source2.hasOwnProperty('d')) {
if (Array.isArray(source2['d'])) {
if (!target2.hasOwnProperty('d') || !Array.isArray(target2['d']) {
= []
target3 else {
} = target2['d']
target3
}
= source2['d']
source3 = 0
count3
for (i = 0, l = source3.length; i < l; i++) {
// should check for nulls and arrays here
if (typeof source3[i] === 'object') {
if (!target3[i] || typeof target3[i] !== 'object') {
= {}
target4 else {
} = target3[i]
target4
}
= source3[i]
source4 = 0
count4
if (source4.hasOwnProperty('e')) {
'e'] = source4['e']
target4[++
count4
}
if (source4.hasOwnProperty('f')) {
'f'] = source4['f']
target4[++
count4
}
if (count4 > 0) {
= target4
target3[i] ++
count3
}else {
} = source3[i]
target3[i] ++
count3
}
}
if (count3 > 0) {
'd'] = target3
target2[++
count2
}
}
}
if (count2 > 0) {
'c'] = target2
target1[++
count1
}
else {
} 'c'] = source1['c']
target1[++
count1
}
}
if (count1 > 0) {
'a'] = target1
target[
}
else {
} 'a'] = source['a']
target[
}
} }
let schema = {
type: 'object',
properties: {
a: {
type: 'object',
properties: {
b: { type: 'number' },
c: {
type: 'object',
properties: {
d: {
type: 'array',
items: [
type: 'integer', default: 3 },
{ type: 'object', properties: { e: { default: 'null' } } }
{ ,
]additionalItems: {
properties: {
e: { default: 'w00t' }
}
}
}
}
}
}
}
}
}
let source = {
a: {
b: {
g: [
,
{},
{},
{}
{}
]
}
}
}
let target = {
a: {
b: {
g: [
,
{},
{},
{}h: 'w00t' }
{
]
}
}
}
function (target, source) {
var source1
var target1
var count1
var source2
var target2
var count2
if (source.hasOwnProperty('a')) {
if (typeof source['a'] === 'object') {
if (!target.hasOwnProperty('a') || typeof target['a'] !== 'object') {
= {}
target1 else {
} = target['a']
target1
}
= source['a']
source1 = 0
count1
if (source1.hasOwnProperty('b')) {
'b'] = source1['b']
target1[++
count1
}
if (source1.hasOwnProperty('c')) {
if (typeof source1['c'] === 'object') {
if (!target1.hasOwnProperty('c') || typeof target1['c'] !== 'object') {
= {}
target2 else {
} = target1['c']
target2
}
= source1['c']
source2 = 0
count2
if (source2.hasOwnProperty('d')) {
if (Array.isArray(source2['d'])) {
if (!target2.hasOwnProperty('d') || !Array.isArray(target2['d']) {
= []
target3 else {
} = target2['d']
target3
}
= source2['d']
source3 = 0
count3
if (0 < source3.length) {
0] = source3[0]
target3[++
count3
}
if (1 < source3.length) {
if (typeof source3[1] === 'object') {
if (1 >= target3.length || typeof target3[1] !== 'object') {
= {}
target4 else {
} = target3[1]
target4
}
= source3[1]
source4 = 0
count4
if (source4.hasOwnProperty('e')) {
'b'] = source4['e']
target4[++
count4
}
if (count4 > 0) {
1] = target4
target3[++
count3
}else {
} 1] = source3[1]
target3[++
count3
}
}
for (i = 3, l = source3.length; i < l; i++) {
// should check for nulls and arrays here
if (typeof source3[i] === 'object') {
if (!target3[i] || typeof target3[i] !== 'object') {
= {}
target4 else {
} = target3[i]
target4
}
= source3[i]
source4 = 0
count4
if (source4.hasOwnProperty('e')) {
'e'] = source4['e']
target4[++
count4else if (options.defaults !== false) {
} 'e'] = 'w00t'
target4[++
count4
}
if (count4 > 0) {
= target4
target3[i] ++
count3
}else {
} = source3[i]
target3[i] ++
count3
}
}
if (count3 > 0) {
'd'] = target3
target2[++
count2
}
}
}
if (count2 > 0) {
'c'] = target2
target1[++
count1
}
else {
} 'c'] = source1['c']
target1[++
count1
}
}
if (count1 > 0) {
'a'] = target1
target[
}
else {
} 'a'] = source['a']
target[
}
} }
let schema = {
type: 'object',
properties: {
a: {
type: 'object',
default: { b: 100 },
properties: {
b: { type: 'number', default: 20 },
c: { type: 'string', default: 'foo' }
}
}
}
}
function (target, source) {
var source0 = source
var target0 = target
var count0 = 0
var source1
var target1
var count1
if (options.defaults !== false) {
'a'] = { b: 100 }
target0[++
count0
}
if (source0.hasOwnProperty('a')) {
if (typeof source['a'] === 'object') {
if (!target0.hasOwnProperty('a') || typeof target0['a'] !== 'object') {
= {}
target1 else {
} = target0['a']
target1
}
= source0['a']
source1 = 0
count1
// EXISTING
// if (source1.hasOwnProperty('b')) {
// target1['b'] = source1['b']
// count1++
// } else if (options.defaults != false) {
// target1['b'] = 20
// count1++
// }
// CHANGES HERE
//-----------------
if (options.defaults !== false) {
'b'] = 20
target1[++
count1
}
if (source1.hasOwnProperty('b')) {
'b'] = source1['b']
target1[++
count1
}
//-----------------
if (options.defaults !== false) {
'c'] = 'foo'
target1[++
count1
}
if (source1.hasOwnProperty('c')) {
'c'] = source1['c']
target1[++
count1
}//-----------------
if (count1 > 0) {
'a'] = target1
target0[++
count0
}
}
} }