JSON Schema-based Object Initialization in JavaScript

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.

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

Initialize with no defaults and filtering additional properties

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) {
  target.a.b = source.a.b
  target.c = source.c
}

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) {
    target.a = {}
  }

  target.a.b = source.a.b
  target.c = source.c
}

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) {
    target.a = {}
  }

  if (source.a && source.a.b) {
    target.a.b = source.a.b
  }

  if (source.c) {
    target.c = source.c
  }
}

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')) {
    target['a'] = {}
  }

  if (source.hasOwnProperty('a') && source.a.hasOwnProperty('b')) {
    target['a']['b'] = source['a']['b']
  }

  if (source.hasOwnProperty('c')) {
    target['c'] = source['c']
  }
}

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')) {
    target['a'] = {}
  }

  let sc = source['a']
  let tc = target['a']

  if (sc && sc.hasOwnProperty('b')) {
    tc['b'] = sc['b']
  }

  if (source.hasOwnProperty('c')) {
    target['c'] = source['c']
  }
}

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')) {
    target['a'] = {}
  }

  let sc = source['a']
  let tc = target['a']

  if (sc) {
    if (sc.hasOwnProperty('b')) {
      tc['b'] = sc['b']
    }

    if (sc.hasOwnProperty('c')) {
      tc['c'] = sc['c']
    }

    if (sc.hasOwnProperty('d')) {
      tc['d'] = sc['d']
    }

    if (sc.hasOwnProperty('e')) {
      tc['e'] = sc['e']
    }
  }

  if (source.hasOwnProperty('f')) {
    target['f'] = source['f']
  }
}

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')) {
    target['a'] = {}
  }

  sc = source['a']
  tc = target['a']

  if (typeof sc === 'object') {
    if (sc.hasOwnProperty('b')) {
      tc['b'] = sc['b']
    }

    if (sc.hasOwnProperty('c')) {
      tc['c'] = sc['c']
    }
  }

  if (source.hasOwnProperty('d') && !target.hasOwnProperty('d')) {
    target['d'] = {}
  }

  sc = source['d']
  tc = target['d']

  if (typeof sc === 'object') {
    if (sc.hasOwnProperty('e')) {
      tc['e'] = sc['e']
    }

    if (sc.hasOwnProperty('f')) {
      tc['f'] = sc['f']
    }
  }

  if (source.hasOwnProperty('g')) {
    target['g'] = source['g']
  }
}

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')) {
    target['a'] = {}
  }

  source1 = source['a']
  target1 = target['a']

  if (typeof source1 === 'object') {
    if (source1.hasOwnProperty('b') && !target1.hasOwnProperty('b')) {
      target1['b'] = {}
    }

    // we can't reuse source1/target1 here because we'll need a reference
    // to it's current value when moving on from `b`.
    source2 = source1['b']
    target2 = target1['b']

    if (typeof source2 === 'object') {
      if (source2.hasOwnProperty('c')) {
        target2['c'] = source2['c']
      }

      if (source2.hasOwnProperty('d')) {
        target2['d'] = source2['d']
      }
    }

    if (source1.hasOwnProperty('e')) {
      target1['e'] = source1['e']
    }
  }

  if (source.hasOwnProperty('f') && !target.hasOwnProperty('f')) {
    target['f'] = {}
  }

  source1 = source['f']
  target1 = target['f']

  if (typeof source1 === 'object') {
    if (source1.hasOwnProperty('g')) {
      target1['g'] = source1['g']
    }
  }
}

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') {
        target['a'] = {}
      }
    } else {
      target['a'] = source['a']
    }
  }

  source1 = source['a']
  target1 = target['a']

  if (typeof source1 === 'object') {
    if (source1.hasOwnProperty('b')) {
      target1['b'] = source1['b']
    }

    if (source1.hasOwnProperty('c')) {
      if (typeof source1['c'] === 'object') {
        if (!target1.hasOwnProperty('c') || typeof target['c'] !== 'object') {
          target1['c'] = {}
        }
      } else {
        target1['c'] = source1['c']
      }
    }

    source2 = source1['c']
    target2 = target1['c']

    if (typeof source2 === 'object') {
      if (source2.hasOwnProperty('d')) {
        target2['d'] = source2['d']
      }

      if (source2.hasOwnProperty('e')) {
        target2['e'] = source2['e']
      }
    }
  }

  if (source.hasOwnProperty('f')) {
    if (typeof source['f'] === 'object') {
      if (!target.hasOwnProperty('f') || typeof target['f'] !== 'object') {
        target['f'] = {}
      }
    } else {
      target['f'] = source['f']
    }
  }

  source1 = source['f']
  target1 = target['f']

  if (typeof source1 === 'object') {
    if (source1.hasOwnProperty('g')) {
      target1['g'] = source1['g']
    }
  }
}

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 {
      target['a'] = source['a']
    }
  }

  source1 = source['a']
  count1 = 0

  if (typeof source1 === 'object') {
    if (source1.hasOwnProperty('b')) {
      target1['b'] = source1['b']
      count1++
    }

    if (source1.hasOwnProperty('c')) {
      if (typeof source1['c'] === 'object') {
        if (!target1.hasOwnProperty('c') || typeof target['c'] !== 'object') {
          target2 = {}
        }
      } else {
        target1['c'] = source1['c']
        count1++
      }
    }

    source2 = source1['c']
    count2 = 0

    if (typeof source2 === 'object') {
      if (source2.hasOwnProperty('d')) {
        target2['d'] = source2['d']
        count2++
      }

      if (source2.hasOwnProperty('e')) {
        target2['e'] = source2['e']
        count2++
      }
    }

    if (count2 > 0) {
      target1['c'] = target2
      count1++
    }
  }

  if (count1 > 0) {
    target['a'] = target1
  }

  if (source.hasOwnProperty('f')) {
    if (typeof source['f'] === 'object') {
      if (!target.hasOwnProperty('f') || typeof target['f'] !== 'object') {
        target1 = {}
      }
    } else {
      target['f'] = source['f']
    }
  }

  source1 = source['f']
  count1 = 0

  if (typeof source1 === 'object') {
    if (source1.hasOwnProperty('g')) {
      target1['g'] = source1['g']
      count1++
    }
  }

  if (count1 > 0) {
    target['f'] = target1
  }
}

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 {
      target['a'] = source['a']
    }
  }

  // this is unnecessary if `source.hasOwnProperty('a')` is false
  source1 = source['a']
  count1 = 0

  // 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 {
        target1 = target['a']
      }

      source1 = source['a']
      count1 = 0

      if (source1.hasOwnProperty('b')) {
        target1['b'] = source1['b']
        count1++
      }

      if (source1.hasOwnProperty('c')) {
        if (typeof source1['c'] === 'object') {
          if (!target1.hasOwnProperty('c') || typeof target['c'] !== 'object') {
            target2 = {}
          } else {
            target2 = target['a']['c']
          }

          source2 = source1['c']
          count2 = 0

          if (source2.hasOwnProperty('d')) {
            target2['d'] = source2['d']
            count2++
          }

          if (source2.hasOwnProperty('e')) {
            target2['e'] = source2['e']
            count2++
          }

          if (count2 > 0) {
            target1['c'] = target2
            count1++
          }

        } else {
          target1['c'] = source1['c']
          count1++
        }
      }

      if (count1 > 0) {
        target['a'] = target1
      }

    } else {
      target['a'] = source['a']
    }
  }

  if (source.hasOwnProperty('f')) {
    if (typeof source['f'] === 'object') {
      if (!target.hasOwnProperty('f') || typeof target['f'] !== 'object') {
        target1 = {}
      } else {
        target1 = target['f']
      }

      source1 = source['f']
      count1 = 0

      if (source1.hasOwnProperty('g')) {
        target1['g'] = source1['g']
        count1++
      }

      if (count1 > 0) {
        target['f'] = target1
      }

    } else {
      target['f'] = source['f']
    }
  }
}

Initialize with no defaults and include additional properties

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)))
}

Initialize with defaults and filtering additional properties

Simplest possible case.

let schema = {
  type: 'object',
  properties: {
    a: { default: 'foo' }
  }
}
function (target, source) {
  target['a'] = source['a'] || 'foo'
}
function (target, source) {
  if (source.hasOwnProperty('a')) {
    target['a'] = source['a']
  } else {
    target['a'] = 'foo'
  }
}
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 {
        target1 = target['a']
      }

      source1 = source['a']
      count1 = 0

      if (source1.hasOwnProperty('b')) {
        target1['b'] = source1['b']
        count1++
      }

      if (source1.hasOwnProperty('c')) {
        if (typeof source1['c'] === 'object') {
          if (!target1.hasOwnProperty('c') || typeof target['c'] !== 'object') {
            target2 = {}
          } else {
            target2 = target['a']['c']
          }

          source2 = source1['c']
          count2 = 0

          if (source2.hasOwnProperty('d')) {
            target2['d'] = source2['d']
            count2++

          // CHANGED TO ADD DEFAULT
          } else if (options.defaults !== false) {
            target2['d'] = false
            count2++
          }

          if (source2.hasOwnProperty('e')) {
            target2['e'] = source2['e']
            count2++
          }

          if (count2 > 0) {
            target1['c'] = target2
            count1++
          }

        } else {
          target1['c'] = source1['c']
          count1++
        }
      }

      if (count1 > 0) {
        target['a'] = target1
      }

    } else {
      target['a'] = source['a']
    }
  }

  if (source.hasOwnProperty('f')) {
    if (typeof source['f'] === 'object') {
      if (!target.hasOwnProperty('f') || typeof target['f'] !== 'object') {
        target1 = {}
      } else {
        target1 = target['f']
      }

      source1 = source['f']
      count1 = 0

      if (source1.hasOwnProperty('g')) {
        target1['g'] = source1['g']
        count1++
      }

      if (count1 > 0) {
        target['f'] = target1
      }

    } else {
      target['f'] = source['f']
    }
  }
}

ARRAY CONTAINER

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 {
        target1 = target['a']
      }

      source1 = source['a']
      count1 = 0

      if (source1.hasOwnProperty('b')) {
        target1['b'] = source1['b']
        count1++
      }

      if (source1.hasOwnProperty('c')) {
        if (typeof source1['c'] === 'object') {
          if (!target1.hasOwnProperty('c') || typeof target1['c'] !== 'object') {
            target2 = {}
          } else {
            target2 = target1['c']
          }

          source2 = source1['c']
          count2 = 0

          if (source2.hasOwnProperty('d')) {
            if (Array.isArray(source2['d'])) {
              if (!target2.hasOwnProperty('d') || !Array.isArray(target2['d']) {
                target3 = []
              } else {
                target3 = target2['d']
              }

              source3 = source2['d']
              count3 = 0

              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 {
                    target4 = target3[i]
                  }

                  source4 = source3[i]
                  count4 = 0


                  if (source4.hasOwnProperty('e')) {
                    target4['e'] = source4['e']
                    count4++
                  }

                  if (source4.hasOwnProperty('f')) {
                    target4['f'] = source4['f']
                    count4++
                  }

                  if (count4 > 0) {
                    target3[i] = target4
                    count3++
                  }
                } else {
                  target3[i] = source3[i]
                  count3++
                }
              }

              if (count3 > 0) {
                target2['d'] = target3
                count2++
              }
            }
          }

          if (count2 > 0) {
            target1['c'] = target2
            count1++
          }

        } else {
          target1['c'] = source1['c']
          count1++
        }
      }

      if (count1 > 0) {
        target['a'] = target1
      }

    } else {
      target['a'] = source['a']
    }
  }
}

Arrays with schemas per index and "additionalItems"

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 {
        target1 = target['a']
      }

      source1 = source['a']
      count1 = 0

      if (source1.hasOwnProperty('b')) {
        target1['b'] = source1['b']
        count1++
      }

      if (source1.hasOwnProperty('c')) {
        if (typeof source1['c'] === 'object') {
          if (!target1.hasOwnProperty('c') || typeof target1['c'] !== 'object') {
            target2 = {}
          } else {
            target2 = target1['c']
          }

          source2 = source1['c']
          count2 = 0

          if (source2.hasOwnProperty('d')) {
            if (Array.isArray(source2['d'])) {
              if (!target2.hasOwnProperty('d') || !Array.isArray(target2['d']) {
                target3 = []
              } else {
                target3 = target2['d']
              }

              source3 = source2['d']
              count3 = 0

              if (0 < source3.length) {
                target3[0] = source3[0]
                count3++
              }
              
              if (1 < source3.length) {
                if (typeof source3[1] === 'object') {
                  if (1 >= target3.length || typeof target3[1] !== 'object') {
                    target4 = {}
                  } else {
                    target4 = target3[1]
                  }
                  
                  source4 = source3[1]
                  count4 = 0
                  
                  if (source4.hasOwnProperty('e')) {
                    target4['b'] = source4['e']
                    count4++
                  }
                  
                  if (count4 > 0) {
                    target3[1] = target4
                    count3++
                  }
                } else {
                  target3[1] = source3[1]
                  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 {
                    target4 = target3[i]
                  }

                  source4 = source3[i]
                  count4 = 0

                  if (source4.hasOwnProperty('e')) {
                    target4['e'] = source4['e']
                    count4++
                  } else if (options.defaults !== false) {
                    target4['e'] = 'w00t'
                    count4++
                  }

                  if (count4 > 0) {
                    target3[i] = target4
                    count3++
                  }
                } else {
                  target3[i] = source3[i]
                  count3++
                }
              }

              if (count3 > 0) {
                target2['d'] = target3
                count2++
              }
            }
          }

          if (count2 > 0) {
            target1['c'] = target2
            count1++
          }

        } else {
          target1['c'] = source1['c']
          count1++
        }
      }

      if (count1 > 0) {
        target['a'] = target1
      }

    } else {
      target['a'] = source['a']
    }
  }
}

Defaults on parent and current nested objects

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) {
    target0['a'] = { b: 100 }
    count0++
  }

  if (source0.hasOwnProperty('a')) {
    if (typeof source['a'] === 'object') {
      if (!target0.hasOwnProperty('a') || typeof target0['a'] !== 'object') {
        target1 = {}
      } else {
        target1 = target0['a']
      }

      source1 = source0['a']
      count1 = 0

      // 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) {
        target1['b'] = 20
        count1++
      }

      if (source1.hasOwnProperty('b')) {
        target1['b'] = source1['b']
        count1++
      }
      
      //-----------------

      if (options.defaults !== false) {
        target1['c'] = 'foo'
        count1++
      }

      if (source1.hasOwnProperty('c')) {
        target1['c'] = source1['c']
        count1++
      }
      //-----------------

      if (count1 > 0) {
        target0['a'] = target1
        count0++
      }
    }
  }
}