diff --git a/README.md b/README.md
index 88ec1a09..6c78e2b0 100644
--- a/README.md
+++ b/README.md
@@ -121,6 +121,7 @@ const stringify = fastJson(mySchema, {
 - `rounding`: setup how the `integer` types will be rounded when not integers. [More details](#integer)
 - `largeArrayMechanism`: set the mechanism that should be used to handle large
 (by default `20000` or more items) arrays. [More details](#largearrays)
+- `enableToJSON`: enable the `toJSON` method check for the generated function.
 
 
 <a name="api"></a>
@@ -669,7 +670,7 @@ const debugCompiled = fastJson({
 
 console.log(debugCompiled) // it is a object contain code, ajv instance
 const rawString = debugCompiled.code // it is the generated code
-console.log(rawString) 
+console.log(rawString)
 
 const stringify = fastJson.restore(debugCompiled) // use the generated string to get back the `stringify` function
 console.log(stringify({ firstName: 'Foo', surname: 'bar' })) // '{"firstName":"Foo"}'
diff --git a/index.js b/index.js
index 46aa5f10..925954de 100644
--- a/index.js
+++ b/index.js
@@ -73,6 +73,7 @@ function resolveRef (location, ref) {
 const arrayItemsReferenceSerializersMap = new Map()
 const objectReferenceSerializersMap = new Map()
 
+let enableToJSON = null
 let rootSchemaId = null
 let refResolver = null
 let validator = null
@@ -88,6 +89,7 @@ function build (schema, options) {
   refResolver = new RefResolver()
   validator = new Validator(options.ajv)
 
+  enableToJSON = options.enableToJSON || false
   rootSchemaId = schema.$id || randomUUID()
 
   isValidSchema(schema)
@@ -527,13 +529,6 @@ function addIfThenElse (location) {
   return code
 }
 
-function toJSON (variableName) {
-  return `(${variableName} && typeof ${variableName}.toJSON === 'function')
-    ? ${variableName}.toJSON()
-    : ${variableName}
-  `
-}
-
 function buildObject (location) {
   const schema = location.schema
 
@@ -551,7 +546,7 @@ function buildObject (location) {
   `
 
   functionCode += `
-      var obj = ${toJSON('input')}
+      var obj = input
       var json = '{'
       var addComma = false
   `
@@ -626,19 +621,14 @@ function buildArray (location) {
 
   if (Array.isArray(itemsSchema)) {
     for (let i = 0; i < itemsSchema.length; i++) {
-      const item = itemsSchema[i]
       const tmpRes = buildValue(mergeLocation(itemsLocation, i), `obj[${i}]`)
       functionCode += `
         if (${i} < arrayLength) {
-          if (${buildArrayTypeCondition(item.type, `[${i}]`)}) {
-            let json = ''
-            ${tmpRes}
-            jsonOutput += json
-            if (${i} < arrayLength - 1) {
-              jsonOutput += ','
-            }
-          } else {
-            throw new Error(\`Item at ${i} does not match schema definition.\`)
+          let json = ''
+          ${tmpRes}
+          jsonOutput += json
+          if (${i} < arrayLength - 1) {
+            jsonOutput += ','
           }
         }
         `
@@ -675,49 +665,12 @@ function buildArray (location) {
   return functionName
 }
 
-function buildArrayTypeCondition (type, accessor) {
-  let condition
-  switch (type) {
-    case 'null':
-      condition = `obj${accessor} === null`
-      break
-    case 'string':
-      condition = `typeof obj${accessor} === 'string'`
-      break
-    case 'integer':
-      condition = `Number.isInteger(obj${accessor})`
-      break
-    case 'number':
-      condition = `Number.isFinite(obj${accessor})`
-      break
-    case 'boolean':
-      condition = `typeof obj${accessor} === 'boolean'`
-      break
-    case 'object':
-      condition = `obj${accessor} && typeof obj${accessor} === 'object' && obj${accessor}.constructor === Object`
-      break
-    case 'array':
-      condition = `Array.isArray(obj${accessor})`
-      break
-    default:
-      if (Array.isArray(type)) {
-        const conditions = type.map((subType) => {
-          return buildArrayTypeCondition(subType, accessor)
-        })
-        condition = `(${conditions.join(' || ')})`
-      } else {
-        throw new Error(`${type} unsupported`)
-      }
-  }
-  return condition
-}
-
 let genFuncNameCounter = 0
 function generateFuncName () {
   return 'anonymous' + genFuncNameCounter++
 }
 
-function buildMultiTypeSerializer (location, input) {
+function buildMultiTypeSerializer (location) {
   const schema = location.schema
   const types = schema.type.sort(t1 => t1 === 'null' ? -1 : 1)
 
@@ -727,26 +680,26 @@ function buildMultiTypeSerializer (location, input) {
   types.forEach((type, index) => {
     const statement = index === 0 ? 'if' : 'else if'
     locationClone.schema.type = type
-    const nestedResult = buildSingleTypeSerializer(locationClone, input)
+    const nestedResult = buildSingleTypeSerializer(locationClone)
     switch (type) {
       case 'null':
         code += `
-          ${statement} (${input} === null)
+          ${statement} (input === null)
             ${nestedResult}
           `
         break
       case 'string': {
         code += `
           ${statement}(
-            typeof ${input} === "string" ||
-            ${input} === null ||
-            ${input} instanceof Date ||
-            ${input} instanceof RegExp ||
+            typeof input === "string" ||
+            input === null ||
+            input instanceof Date ||
+            input instanceof RegExp ||
             (
-              typeof ${input} === "object" &&
-              typeof ${input}.toString === "function" &&
-              ${input}.toString !== Object.prototype.toString &&
-              !(${input} instanceof Date)
+              typeof input === "object" &&
+              typeof input.toString === "function" &&
+              input.toString !== Object.prototype.toString &&
+              !(input instanceof Date)
             )
           )
             ${nestedResult}
@@ -755,21 +708,21 @@ function buildMultiTypeSerializer (location, input) {
       }
       case 'array': {
         code += `
-          ${statement}(Array.isArray(${input}))
+          ${statement}(Array.isArray(input))
             ${nestedResult}
         `
         break
       }
       case 'integer': {
         code += `
-          ${statement}(Number.isInteger(${input}) || ${input} === null)
+          ${statement}(Number.isInteger(input) || input === null)
             ${nestedResult}
         `
         break
       }
       default: {
         code += `
-          ${statement}(typeof ${input} === "${type}" || ${input} === null)
+          ${statement}(typeof input === "${type}" || input === null)
             ${nestedResult}
         `
         break
@@ -777,13 +730,13 @@ function buildMultiTypeSerializer (location, input) {
     }
   })
   code += `
-    else throw new Error(\`The value $\{JSON.stringify(${input})} does not match schema definition.\`)
+    else throw new Error(\`The value $\{JSON.stringify(input)} does not match schema definition.\`)
   `
 
   return code
 }
 
-function buildSingleTypeSerializer (location, input) {
+function buildSingleTypeSerializer (location) {
   const schema = location.schema
 
   switch (schema.type) {
@@ -791,37 +744,37 @@ function buildSingleTypeSerializer (location, input) {
       return 'json += \'null\''
     case 'string': {
       if (schema.format === 'date-time') {
-        return `json += serializer.asDateTime(${input})`
+        return 'json += serializer.asDateTime(input)'
       } else if (schema.format === 'date') {
-        return `json += serializer.asDate(${input})`
+        return 'json += serializer.asDate(input)'
       } else if (schema.format === 'time') {
-        return `json += serializer.asTime(${input})`
+        return 'json += serializer.asTime(input)'
       } else {
-        return `json += serializer.asString(${input})`
+        return 'json += serializer.asString(input)'
       }
     }
     case 'integer':
-      return `json += serializer.asInteger(${input})`
+      return 'json += serializer.asInteger(input)'
     case 'number':
-      return `json += serializer.asNumber(${input})`
+      return 'json += serializer.asNumber(input)'
     case 'boolean':
-      return `json += serializer.asBoolean(${input})`
+      return 'json += serializer.asBoolean(input)'
     case 'object': {
       const funcName = buildObject(location)
-      return `json += ${funcName}(${input})`
+      return `json += ${funcName}(input)`
     }
     case 'array': {
       const funcName = buildArray(location)
-      return `json += ${funcName}(${input})`
+      return `json += ${funcName}(input)`
     }
     case undefined:
-      return `json += JSON.stringify(${input})`
+      return 'json += JSON.stringify(input)'
     default:
       throw new Error(`${schema.type} unsupported`)
   }
 }
 
-function buildConstSerializer (location, input) {
+function buildConstSerializer (location) {
   const schema = location.schema
   const type = schema.type
 
@@ -831,7 +784,7 @@ function buildConstSerializer (location, input) {
 
   if (hasNullType) {
     code += `
-      if (${input} === null) {
+      if (input === null) {
         json += 'null'
       } else {
     `
@@ -851,8 +804,13 @@ function buildConstSerializer (location, input) {
 function buildValue (location, input) {
   let schema = location.schema
 
+  let code = `
+    var input = ${input}
+  `
+
   if (typeof schema === 'boolean') {
-    return `json += JSON.stringify(${input})`
+    code += 'json += JSON.stringify(input)'
+    return code
   }
 
   if (schema.$ref) {
@@ -876,7 +834,13 @@ function buildValue (location, input) {
 
   const type = schema.type
 
-  let code = ''
+  if (enableToJSON === true) {
+    code += `
+      if (input && typeof input.toJSON === 'function' && !(input instanceof Date)) {
+        input = input.toJSON()
+      }
+    `
+  }
 
   if (type === undefined && (schema.anyOf || schema.oneOf)) {
     const type = schema.anyOf ? 'anyOf' : 'oneOf'
@@ -887,13 +851,14 @@ function buildValue (location, input) {
       const schemaRef = optionLocation.schemaId + optionLocation.jsonPointer
       const nestedResult = buildValue(optionLocation, input)
       code += `
-        ${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", ${input}))
+        ${index === 0 ? 'if' : 'else if'}(validator.validate("${schemaRef}", input)) {
           ${nestedResult}
+        }
       `
     }
 
     code += `
-      else throw new Error(\`The value $\{JSON.stringify(${input})} does not match schema definition.\`)
+      else throw new Error(\`The value $\{JSON.stringify(input)} does not match schema definition.\`)
     `
     return code
   }
@@ -901,18 +866,18 @@ function buildValue (location, input) {
   const nullable = schema.nullable === true
   if (nullable) {
     code += `
-      if (${input} === null) {
+      if (input === null) {
         json += 'null'
       } else {
     `
   }
 
   if (schema.const !== undefined) {
-    code += buildConstSerializer(location, input)
+    code += buildConstSerializer(location)
   } else if (Array.isArray(type)) {
-    code += buildMultiTypeSerializer(location, input)
+    code += buildMultiTypeSerializer(location)
   } else {
-    code += buildSingleTypeSerializer(location, input)
+    code += buildSingleTypeSerializer(location)
   }
 
   if (nullable) {
diff --git a/test/array.test.js b/test/array.test.js
index b1fe96ff..d844be8f 100644
--- a/test/array.test.js
+++ b/test/array.test.js
@@ -1,6 +1,7 @@
 'use strict'
 
 const test = require('tap').test
+const { DateTime } = require('luxon')
 const validator = require('is-my-json-valid')
 const build = require('..')
 
@@ -152,28 +153,56 @@ buildTest({
   '@data': ['test']
 })
 
-test('invalid items throw', (t) => {
+test('coerce number to string type item', (t) => {
   t.plan(1)
   const schema = {
-    type: 'object',
-    properties: {
-      args: {
-        type: 'array',
-        items: [
-          {
-            type: 'object',
-            patternProperties: {
-              '.*': {
-                type: 'string'
-              }
-            }
-          }
-        ]
-      }
-    }
+    type: 'array',
+    items: [{ type: 'string' }]
+  }
+  const stringify = build(schema)
+  t.equal(stringify([1]), '["1"]')
+})
+
+test('coerce string to number type item', (t) => {
+  t.plan(1)
+  const schema = {
+    type: 'array',
+    items: [{ type: 'number' }]
+  }
+  const stringify = build(schema)
+  t.equal(stringify(['1']), '[1]')
+})
+
+test('coerce string to integer type item', (t) => {
+  t.plan(1)
+  const schema = {
+    type: 'array',
+    items: [{ type: 'integer' }]
+  }
+  const stringify = build(schema)
+  t.equal(stringify(['1']), '[1]')
+})
+
+test('coerce date to string (date) type item', (t) => {
+  t.plan(1)
+  const schema = {
+    type: 'array',
+    items: [{ type: 'string', format: 'date' }]
+  }
+  const stringify = build(schema)
+  const date = new Date()
+  t.equal(stringify([date]), `["${DateTime.fromJSDate(date).toISODate()}"]`)
+})
+
+test('coerce date to string (time) type item', (t) => {
+  t.plan(1)
+  const schema = {
+    type: 'array',
+    items: [{ type: 'string', format: 'time' }]
   }
   const stringify = build(schema)
-  t.throws(() => stringify({ args: ['invalid'] }))
+  const date = new Date()
+  t.equal(stringify([date]), `["${DateTime.fromJSDate(date).toFormat('HH:mm:ss')}"]`)
 })
 
 buildTest({
diff --git a/test/toJSON.test.js b/test/toJSON.test.js
index 04ac2975..c24b2503 100644
--- a/test/toJSON.test.js
+++ b/test/toJSON.test.js
@@ -3,6 +3,27 @@
 const test = require('tap').test
 const build = require('..')
 
+test('should not call toJSON when enableToJSON equals false', (t) => {
+  t.plan(1)
+
+  const schema = {
+    type: 'object',
+    properties: {
+      data: {
+        type: 'object'
+      }
+    }
+  }
+
+  const stringify = build(schema, { enableToJSON: false })
+
+  const data = {
+    data: { toJSON () { return 4 } }
+  }
+
+  t.equal(stringify(data), '{"data":{}}')
+})
+
 test('use toJSON method on object types', (t) => {
   t.plan(1)
 
@@ -14,7 +35,10 @@ test('use toJSON method on object types', (t) => {
         type: 'string'
       }
     }
+  }, {
+    enableToJSON: true
   })
+
   const object = {
     product: { name: 'cola' },
     toJSON: function () {
@@ -39,7 +63,10 @@ test('use toJSON method on nested object types', (t) => {
         }
       }
     }
+  }, {
+    enableToJSON: true
   })
+
   const array = [
     {
       product: { name: 'cola' },
@@ -74,7 +101,10 @@ test('not use toJSON if does not exist', (t) => {
         }
       }
     }
+  }, {
+    enableToJSON: true
   })
+
   const object = {
     product: { name: 'cola' }
   }
@@ -99,7 +129,10 @@ test('not fail on null object declared nullable', (t) => {
         }
       }
     }
+  }, {
+    enableToJSON: true
   })
+
   t.equal('null', stringify(null))
 })
 
@@ -120,7 +153,10 @@ test('not fail on null sub-object declared nullable', (t) => {
         }
       }
     }
+  }, {
+    enableToJSON: true
   })
+
   const object = {
     product: null
   }
@@ -144,7 +180,10 @@ test('throw an error on non nullable null sub-object', (t) => {
         }
       }
     }
+  }, {
+    enableToJSON: true
   })
+
   const object = {
     product: null
   }
@@ -169,6 +208,1093 @@ test('throw an error on non nullable null object', (t) => {
         }
       }
     }
+  }, {
+    enableToJSON: true
   })
+
   t.throws(() => { stringify(null) })
 })
+
+test('recursive toJSON call', (t) => {
+  t.plan(1)
+
+  const schema = {
+    type: 'object',
+    properties: {
+      firstName: {
+        type: 'string'
+      },
+      lastName: {
+        type: ['string', 'null']
+      },
+      age: {
+        type: 'integer',
+        minimum: 0
+      }
+    }
+  }
+
+  const stringify = build(schema, { enableToJSON: true })
+
+  const data = {
+    props: {
+      firstName: { toJSON () { return 'Matteo' } },
+      lastName: { toJSON () { return 'Collina' } },
+      age: { toJSON () { return 32 } }
+    },
+    toJSON () {
+      return {
+        firstName: this.props.firstName,
+        lastName: this.props.lastName,
+        age: this.props.age
+      }
+    }
+  }
+
+  t.equal(stringify(data), JSON.stringify(data))
+})
+
+test('toJSON - ref - properties', (t) => {
+  t.plan(2)
+
+  const schema = {
+    title: 'object with $ref',
+    definitions: {
+      def: {
+        type: 'object',
+        properties: {
+          str: {
+            type: 'string'
+          }
+        }
+      }
+    },
+    type: 'object',
+    properties: {
+      obj: {
+        $ref: '#/definitions/def'
+      }
+    }
+  }
+
+  const object = {
+    props: {
+      obj: {
+        toJSON () {
+          t.pass()
+          return { str: 'test' }
+        }
+      }
+    },
+    toJSON () {
+      return { obj: this.props.obj }
+    }
+  }
+
+  const stringify = build(schema, { enableToJSON: true })
+  const output = stringify(object)
+
+  JSON.parse(output)
+
+  t.equal(output, '{"obj":{"str":"test"}}')
+})
+
+test('toJSON - ref - items', (t) => {
+  t.plan(2)
+
+  const schema = {
+    title: 'array with $ref',
+    definitions: {
+      def: {
+        type: 'object',
+        properties: {
+          str: {
+            type: 'string'
+          }
+        }
+      }
+    },
+    type: 'array',
+    items: { $ref: '#/definitions/def' }
+  }
+
+  const arrayObj = {
+    props: {
+      str: {
+        toJSON () { return { str: 'test' } }
+      }
+    },
+    toJSON () { return [this.props.str] }
+  }
+
+  const stringify = build(schema, { enableToJSON: true })
+  const output = stringify(arrayObj)
+
+  JSON.parse(output)
+  t.pass()
+
+  t.equal(output, '[{"str":"test"}]')
+})
+
+test('toJSON - ref - patternProperties', (t) => {
+  t.plan(2)
+
+  const schema = {
+    title: 'object with $ref',
+    definitions: {
+      def: {
+        type: 'object',
+        properties: {
+          str: {
+            type: 'string'
+          }
+        }
+      }
+    },
+    type: 'object',
+    properties: {},
+    patternProperties: {
+      obj: {
+        $ref: '#/definitions/def'
+      }
+    }
+  }
+
+  const object = {
+    props: {
+      obj: {
+        toJSON () { return { str: 'test' } }
+      }
+    },
+    toJSON () {
+      return { obj: this.props.obj }
+    }
+  }
+
+  const stringify = build(schema, { enableToJSON: true })
+  const output = stringify(object)
+
+  JSON.parse(output)
+  t.pass()
+
+  t.equal(output, '{"obj":{"str":"test"}}')
+})
+
+test('toJSON - ref - additionalProperties', (t) => {
+  t.plan(2)
+
+  const schema = {
+    title: 'object with $ref',
+    definitions: {
+      def: {
+        type: 'object',
+        properties: {
+          str: {
+            type: 'string'
+          }
+        }
+      }
+    },
+    type: 'object',
+    properties: {},
+    additionalProperties: {
+      $ref: '#/definitions/def'
+    }
+  }
+
+  const object = {
+    props: {
+      obj: {
+        toJSON () { return { str: 'test' } }
+      }
+    },
+    toJSON () {
+      return { obj: this.props.obj }
+    }
+  }
+
+  const stringify = build(schema, { enableToJSON: true })
+  const output = stringify(object)
+
+  JSON.parse(output)
+  t.pass()
+
+  t.equal(output, '{"obj":{"str":"test"}}')
+})
+
+test('toJSON - ref - pattern-additional Properties', (t) => {
+  t.plan(2)
+
+  const schema = {
+    title: 'object with $ref',
+    definitions: {
+      def: {
+        type: 'object',
+        properties: {
+          str: {
+            type: 'string'
+          }
+        }
+      }
+    },
+    type: 'object',
+    properties: {},
+    patternProperties: {
+      reg: {
+        $ref: '#/definitions/def'
+      }
+    },
+    additionalProperties: {
+      $ref: '#/definitions/def'
+    }
+  }
+
+  const object = {
+    props: {
+      patternObj: {
+        toJSON () { return { str: 'test' } }
+      }
+    },
+    toJSON () {
+      return {
+        reg: this.props.patternObj,
+        obj: this.props.patternObj
+      }
+    }
+  }
+
+  const stringify = build(schema, { enableToJSON: true })
+  const output = stringify(object)
+
+  JSON.parse(output)
+  t.pass()
+
+  t.equal(output, '{"reg":{"str":"test"},"obj":{"str":"test"}}')
+})
+
+test('toJSON - ref - deepObject schema', (t) => {
+  t.plan(2)
+
+  const schema = {
+    title: 'object with $ref',
+    definitions: {
+      def: {
+        type: 'object',
+        properties: {
+          coming: {
+            type: 'object',
+            properties: {
+              where: {
+                type: 'string'
+              }
+            }
+          }
+        }
+      }
+    },
+    type: 'object',
+    properties: {
+      winter: {
+        type: 'object',
+        properties: {
+          is: {
+            $ref: '#/definitions/def'
+          }
+        }
+      }
+    }
+  }
+
+  const where = {
+    toJSON () { return 'to town' }
+  }
+  const coming = {
+    toJSON () { return { where } }
+  }
+  const is = {
+    toJSON () { return { coming } }
+  }
+  const winter = {
+    toJSON () { return { is } }
+  }
+  const object = {
+    toJSON () { return { winter } }
+  }
+
+  const stringify = build(schema, { enableToJSON: true })
+  const output = stringify(object)
+
+  JSON.parse(output)
+  t.pass()
+
+  t.equal(output, '{"winter":{"is":{"coming":{"where":"to town"}}}}')
+})
+
+test('toJSON - ref - Regression 2.5.2', t => {
+  t.plan(1)
+
+  const externalSchema = {
+    '/models/Bar': {
+      $id: '/models/Bar',
+      $schema: 'http://json-schema.org/schema#',
+      definitions: {
+        entity: {
+          type: 'object',
+          properties: { field: { type: 'string' } }
+        }
+      }
+    },
+    '/models/Foo': {
+      $id: '/models/Foo',
+      $schema: 'http://json-schema.org/schema#',
+      definitions: {
+        entity: {
+          type: 'object',
+          properties: {
+            field: { type: 'string' },
+            sub: {
+              oneOf: [
+                { $ref: '/models/Bar#/definitions/entity' },
+                { type: 'null' }
+              ]
+            }
+          }
+        }
+      }
+    }
+  }
+
+  const schema = {
+    type: 'array',
+    items: {
+      $ref: '/models/Foo#/definitions/entity'
+    }
+  }
+
+  const object = {
+    props: {
+      field: {
+        toJSON () { return 'parent' }
+      },
+      sub: {
+        toJSON () { return { field: 'joined' } }
+      }
+    },
+    toJSON () {
+      return [{
+        field: this.props.field,
+        sub: this.props.sub
+      }]
+    }
+  }
+
+  const stringify = build(schema, { schema: externalSchema, enableToJSON: true })
+  const output = stringify(object)
+
+  t.equal(output, '[{"field":"parent","sub":{"field":"joined"}}]')
+})
+
+test('possibly nullable integer primitive alternative', (t) => {
+  t.plan(1)
+
+  const schema = {
+    title: 'simple object with multi-type nullable primitive',
+    type: 'object',
+    properties: {
+      data: {
+        type: ['integer']
+      }
+    }
+  }
+
+  const stringify = build(schema, { ajv: { allowUnionTypes: true }, enableToJSON: true })
+
+  const data = {
+    data: { toJSON () { return 4 } }
+  }
+
+  t.equal(stringify(data), '{"data":4}')
+})
+
+test('possibly nullable number primitive alternative', (t) => {
+  t.plan(1)
+
+  const schema = {
+    title: 'simple object with multi-type nullable primitive',
+    type: 'object',
+    properties: {
+      data: {
+        type: ['number']
+      }
+    }
+  }
+
+  const stringify = build(schema, { enableToJSON: true })
+
+  const data = {
+    data: { toJSON () { return 4 } }
+  }
+
+  t.equal(stringify(data), '{"data":4}')
+})
+
+test('possibly nullable integer primitive alternative with null value', (t) => {
+  t.plan(1)
+
+  const schema = {
+    title: 'simple object with multi-type nullable primitive',
+    type: 'object',
+    properties: {
+      data: {
+        type: ['integer']
+      }
+    }
+  }
+
+  const stringify = build(schema, { enableToJSON: true })
+
+  const dataNull = {
+    data: { toJSON () { return null } }
+  }
+
+  const value = stringify(dataNull)
+  t.equal(value, '{"data":0}')
+})
+
+test('possibly nullable number primitive alternative with null value', (t) => {
+  t.plan(1)
+
+  const schema = {
+    title: 'simple object with multi-type nullable primitive',
+    type: 'object',
+    properties: {
+      data: {
+        type: ['number']
+      }
+    }
+  }
+
+  const stringify = build(schema, { enableToJSON: true })
+
+  const dataNull = {
+    data: { toJSON () { return null } }
+  }
+
+  const value = stringify(dataNull)
+  t.equal(value, '{"data":0}')
+})
+
+test('nullable integer primitive', (t) => {
+  t.plan(1)
+
+  const schema = {
+    title: 'simple object with nullable primitive',
+    type: 'object',
+    properties: {
+      data: {
+        type: ['integer', 'null']
+      }
+    }
+  }
+
+  const stringify = build(schema, { enableToJSON: true })
+
+  const data = {
+    data: { toJSON () { return 4 } }
+  }
+
+  const value = stringify(data)
+  t.equal(value, '{"data":4}')
+})
+
+test('nullable number primitive', (t) => {
+  t.plan(1)
+
+  const schema = {
+    title: 'simple object with nullable primitive',
+    type: 'object',
+    properties: {
+      data: {
+        type: ['number', 'null']
+      }
+    }
+  }
+
+  const stringify = build(schema, { enableToJSON: true })
+
+  const data = {
+    data: { toJSON () { return 4 } }
+  }
+
+  const value = stringify(data)
+  t.equal(value, '{"data":4}')
+})
+
+test('nullable primitive with null value', (t) => {
+  t.plan(1)
+
+  const schema = {
+    title: 'simple object with nullable primitive',
+    type: 'object',
+    properties: {
+      data: {
+        type: ['integer', 'null']
+      }
+    }
+  }
+
+  const stringify = build(schema, { enableToJSON: true })
+
+  const dataNull = {
+    data: { toJSON () { return null } }
+  }
+
+  const value = stringify(dataNull)
+  t.equal(value, '{"data":null}')
+})
+
+test('nullable number primitive with null value', (t) => {
+  t.plan(1)
+
+  const schema = {
+    title: 'simple object with nullable primitive',
+    type: 'object',
+    properties: {
+      data: {
+        type: ['number', 'null']
+      }
+    }
+  }
+
+  const stringify = build(schema, { enableToJSON: true })
+
+  const dataNull = {
+    data: { toJSON () { return null } }
+  }
+
+  const value = stringify(dataNull)
+  t.equal(value, '{"data":null}')
+})
+
+test('possibly null object with multi-type property', (t) => {
+  t.plan(3)
+
+  const schema = {
+    title: 'simple object with multi-type property',
+    type: 'object',
+    properties: {
+      objectOrNull: {
+        type: ['object', 'null'],
+        properties: {
+          stringOrNumber: {
+            type: ['string', 'number']
+          }
+        }
+      }
+    }
+  }
+  const stringify = build(schema, { enableToJSON: true })
+
+  const dataString = { toJSON () { return 'string1' } }
+  const dataNumber = { toJSON () { return 42 } }
+  const dataNull = { toJSON () { return null } }
+
+  t.equal(stringify({ objectOrNull: { toJSON () { return { stringOrNumber: dataString } } } }), '{"objectOrNull":{"stringOrNumber":"string1"}}')
+  t.equal(stringify({ objectOrNull: { toJSON () { return { stringOrNumber: dataNumber } } } }), '{"objectOrNull":{"stringOrNumber":42}}')
+  t.equal(stringify({ objectOrNull: dataNull }), '{"objectOrNull":null}')
+})
+
+test('object with possibly null array of multiple types', (t) => {
+  t.plan(5)
+
+  const schema = {
+    title: 'object with array of multiple types',
+    type: 'object',
+    properties: {
+      arrayOfStringsAndNumbers: {
+        type: ['array', 'null'],
+        items: {
+          type: ['string', 'number', 'null']
+        }
+      }
+    }
+  }
+
+  const dataString1 = { toJSON () { return 'string1' } }
+  const dataString2 = { toJSON () { return 'string2' } }
+  const dataNumber1 = { toJSON () { return 42 } }
+  const dataNumber2 = { toJSON () { return 7 } }
+  const dataNull = { toJSON () { return null } }
+
+  const stringify = build(schema, { enableToJSON: true })
+
+  const arrayStringsAndNumberString = {
+    arrayOfStringsAndNumbers: { toJSON () { return [dataString1, dataString2] } }
+  }
+  const arrayStringsAndNumberNumber = {
+    arrayOfStringsAndNumbers: { toJSON () { return [dataNumber1, dataNumber2] } }
+  }
+  const arrayStringsAndNumberMixed = {
+    arrayOfStringsAndNumbers: { toJSON () { return [dataString1, dataNumber1, dataNumber2, dataString2] } }
+  }
+  const arrayStringsAndNumberMixedNull = {
+    arrayOfStringsAndNumbers: { toJSON () { return [dataString1, dataNull, dataNumber1, dataNumber2, dataString2, dataNull] } }
+  }
+
+  try {
+    const value = stringify({ arrayOfStringsAndNumbers: dataNull })
+    t.equal(value, '{"arrayOfStringsAndNumbers":null}')
+  } catch (e) {
+    console.log(e)
+    t.fail()
+  }
+
+  try {
+    const value = stringify(arrayStringsAndNumberString)
+    t.equal(value, '{"arrayOfStringsAndNumbers":["string1","string2"]}')
+  } catch (e) {
+    console.log(e)
+    t.fail()
+  }
+
+  t.equal(stringify(arrayStringsAndNumberNumber), '{"arrayOfStringsAndNumbers":[42,7]}')
+  t.equal(stringify(arrayStringsAndNumberMixed), '{"arrayOfStringsAndNumbers":["string1",42,7,"string2"]}')
+  t.equal(stringify(arrayStringsAndNumberMixedNull), '{"arrayOfStringsAndNumbers":["string1",null,42,7,"string2",null]}')
+})
+
+test('object that is simultaneously a string and a json', (t) => {
+  t.plan(2)
+  const schema = {
+    type: 'object',
+    properties: {
+      simultaneously: {
+        type: ['string', 'object'],
+        properties: {
+          foo: { type: 'string' }
+        }
+      }
+    }
+  }
+
+  const likeObjectId = {
+    toString () { return 'hello' }
+  }
+
+  const stringify = build(schema, { enableToJSON: true })
+  const valueStr = stringify({ simultaneously: { toJSON () { return likeObjectId } } })
+  t.equal(valueStr, '{"simultaneously":"hello"}')
+
+  const valueObj = stringify({ simultaneously: { toJSON () { return { foo: likeObjectId } } } })
+  t.equal(valueObj, '{"simultaneously":{"foo":"hello"}}')
+})
+
+test('object that is simultaneously a string and a json switched', (t) => {
+  t.plan(2)
+  const schema = {
+    type: 'object',
+    properties: {
+      simultaneously: {
+        type: ['object', 'string'],
+        properties: {
+          foo: { type: 'string' }
+        }
+      }
+    }
+  }
+
+  const likeObjectId = {
+    toString () { return 'hello' }
+  }
+
+  const stringify = build(schema, { enableToJSON: true })
+  const valueStr = stringify({ simultaneously: { toJSON () { return likeObjectId } } })
+  t.equal(valueStr, '{"simultaneously":{}}')
+
+  const valueObj = stringify({ simultaneously: { toJSON () { return { foo: likeObjectId } } } })
+  t.equal(valueObj, '{"simultaneously":{"foo":"hello"}}')
+})
+
+test('should throw an error when type is array and object is null', (t) => {
+  t.plan(1)
+  const schema = {
+    type: 'object',
+    properties: {
+      arr: {
+        type: 'array',
+        items: {
+          type: 'number'
+        }
+      }
+    }
+  }
+
+  const dataNull = { toJSON () { return null } }
+
+  const stringify = build(schema, { enableToJSON: true })
+  t.throws(
+    () => stringify({ arr: dataNull }),
+    new TypeError('The value \'null\' does not match schema definition.')
+  )
+})
+
+test('use toJSON method on primary json types', (t) => {
+  t.plan(2)
+
+  const stringify = build({
+    title: 'object of primitive (json) types',
+    type: 'object',
+    properties: {
+      _bool: {
+        type: 'boolean'
+      },
+      _int: {
+        type: 'integer'
+      },
+      _null: {
+        type: 'null'
+      },
+      _num: {
+        type: 'number'
+      },
+      _str: {
+        type: 'string'
+      }
+    }
+  }, {
+    enableToJSON: true
+  })
+
+  const input = {
+    _bool: {
+      toJSON () { return true }
+    },
+    _int: {
+      toJSON () { return 42 }
+    },
+    _null: {
+      toJSON () { return null }
+    },
+    _num: {
+      toJSON () { return 3.14 }
+    },
+    _str: {
+      toJSON () { return 'whatever' }
+    }
+  }
+  const expected = '{"_bool":true,"_int":42,"_null":null,"_num":3.14,"_str":"whatever"}'
+
+  t.equal(JSON.stringify(input), expected)
+  t.equal(stringify(input), expected)
+})
+
+test('toJSON skips missing props when not required', (t) => {
+  t.plan(1)
+
+  const stringify = build({
+    title: 'object of primitive (json) types',
+    type: 'object',
+    properties: {
+      _bool: {
+        type: 'boolean'
+      },
+      _int: {
+        type: 'integer'
+      },
+      _null: {
+        type: 'null'
+      },
+      _num: {
+        type: 'number'
+      },
+      _str: {
+        type: 'string'
+      }
+    }
+  }, {
+    enableToJSON: true
+  })
+
+  const input = {
+    toJSON () { return {} }
+  }
+  const expected = '{}'
+  t.equal(stringify(input), expected)
+})
+
+test('toJSON forwards nullable types', (t) => {
+  t.plan(4)
+
+  const nullable = true
+  const stringify = build({
+    title: 'object of nullable primitive (json) types',
+    type: 'object',
+    properties: {
+      _bool: {
+        type: 'boolean',
+        nullable
+      },
+      _int: {
+        type: 'integer',
+        nullable
+      },
+      _num: {
+        type: 'number',
+        nullable
+      },
+      _str: {
+        type: 'string',
+        nullable
+      }
+    }
+  }, {
+    enableToJSON: true
+  })
+
+  const inputNull = {
+    _bool: {
+      toJSON () { return null }
+    },
+    _int: {
+      toJSON () { return null }
+    },
+    _num: {
+      toJSON () { return null }
+    },
+    _str: {
+      toJSON () { return null }
+    }
+  }
+  const expectedNull = '{"_bool":null,"_int":null,"_num":null,"_str":null}'
+  t.equal(JSON.stringify(inputNull), expectedNull)
+  t.equal(stringify(inputNull), expectedNull)
+
+  const inputNotNull = {
+    _bool: {
+      toJSON () { return true }
+    },
+    _int: {
+      toJSON () { return 42 }
+    },
+    _num: {
+      toJSON () { return 3.14 }
+    },
+    _str: {
+      toJSON () { return 'whatever' }
+    }
+  }
+  const expectedNotNull = '{"_bool":true,"_int":42,"_num":3.14,"_str":"whatever"}'
+  t.equal(JSON.stringify(inputNotNull), expectedNotNull)
+  t.equal(stringify(inputNotNull), expectedNotNull)
+})
+
+test('toJSON supports required types', (t) => {
+  t.plan(2)
+
+  const stringify = build({
+    title: 'object of required primitive (json) types',
+    type: 'object',
+    properties: {
+      _bool: {
+        type: 'boolean'
+      },
+      _int: {
+        type: 'integer'
+      },
+      _null: {
+        type: 'null'
+      },
+      _num: {
+        type: 'number'
+      },
+      _str: {
+        type: 'string'
+      }
+    },
+    required: ['_bool', '_int', '_null', '_num', '_str']
+  }, {
+    enableToJSON: true
+  })
+
+  const input = {
+    _bool: {
+      toJSON () { return true }
+    },
+    _int: {
+      toJSON () { return 42 }
+    },
+    _null: {
+      toJSON () { return null }
+    },
+    _num: {
+      toJSON () { return 3.14 }
+    },
+    _str: {
+      toJSON () { return 'whatever' }
+    }
+  }
+  const expected = '{"_bool":true,"_int":42,"_null":null,"_num":3.14,"_str":"whatever"}'
+  t.equal(stringify(input), expected)
+
+  const invalidInput = {
+    toJSON () { return {} }
+  }
+  t.throws(() => { stringify(invalidInput) })
+})
+
+test('use toJSON recursively', (t) => {
+  t.plan(2)
+  const nullable = true
+  const stringify = build({
+    title: 'simple object',
+    type: 'object',
+    properties: {
+      _bool: {
+        type: 'boolean'
+      },
+      _bool_nullable: {
+        nullable,
+        type: 'boolean'
+      },
+      _bool_required: {
+        type: 'boolean'
+      },
+      _int: {
+        type: 'integer'
+      },
+      _int_nullable: {
+        nullable,
+        type: 'integer'
+      },
+      _int_required: {
+        type: 'integer'
+      },
+      _null: {
+        type: 'null'
+      },
+      _null_required: {
+        type: 'null'
+      },
+      _num: {
+        type: 'number'
+      },
+      _num_nullable: {
+        nullable,
+        type: 'number'
+      },
+      _num_required: {
+        type: 'number'
+      },
+      _str: {
+        type: 'string'
+      },
+      _str_nullable: {
+        nullable,
+        type: 'string'
+      },
+      _str_required: {
+        type: 'string'
+      }
+    },
+    required: [
+      '_bool_required',
+      '_int_required',
+      '_null_required',
+      '_num_required',
+      '_str_required'
+    ]
+  }, {
+    enableToJSON: true
+  })
+  const aggregate = {
+    props: {
+      _bool: {
+        toJSON () { return true }
+      },
+      _int: {
+        toJSON () { return 42 }
+      },
+      _null: {
+        toJSON () { return null }
+      },
+      _num: {
+        toJSON () { return 3.14 }
+      },
+      _str: {
+        toJSON () { return 'whatever' }
+      }
+    },
+    toJSON () {
+      return {
+        _bool: this.props._bool,
+        _bool_nullable: this.props._null,
+        _bool_required: this.props._bool,
+        _int: this.props._int,
+        _int_nullable: this.props._null,
+        _int_required: this.props._int,
+        _null: this.props._null,
+        _null_required: this.props._null,
+        _num: this.props._num,
+        _num_nullable: this.props._null,
+        _num_required: this.props._num,
+        _str: this.props._str,
+        _str_nullable: this.props._null,
+        _str_required: this.props._str
+      }
+    }
+  }
+  const expected = '{"_bool":true,"_bool_nullable":null,"_bool_required":true,"_int":42,"_int_nullable":null,"_int_required":42,"_null":null,"_null_required":null,"_num":3.14,"_num_nullable":null,"_num_required":3.14,"_str":"whatever","_str_nullable":null,"_str_required":"whatever"}'
+
+  t.equal(JSON.stringify(aggregate), expected)
+  t.equal(stringify(aggregate), expected)
+})
+
+test('object with tuple of multiple types', (t) => {
+  t.plan(2)
+
+  const schema = {
+    title: 'object with array of multiple types',
+    type: 'object',
+    properties: {
+      fixedTupleOfStringsAndNumbers: {
+        type: 'array',
+        items: [
+          {
+            type: 'string'
+          },
+          {
+            type: 'number'
+          },
+          {
+            type: ['string', 'number']
+          }
+        ]
+      }
+    }
+  }
+  const stringify = build(schema, { enableToJSON: true })
+
+  const dataString1 = { toJSON () { return 'string1' } }
+  const dataString2 = { toJSON () { return 'string2' } }
+  const dataNumber42 = { toJSON () { return 42 } }
+  const dataNumber7 = { toJSON () { return 7 } }
+
+  try {
+    const value = stringify({
+      fixedTupleOfStringsAndNumbers: { toJSON () { return [dataString1, dataNumber42, dataNumber7] } }
+    })
+    t.equal(value, '{"fixedTupleOfStringsAndNumbers":["string1",42,7]}')
+  } catch (e) {
+    console.log(e)
+    t.fail()
+  }
+
+  try {
+    const value = JSON.stringify({
+      fixedTupleOfStringsAndNumbers: { toJSON () { return [dataString1, dataNumber42, dataString2] } }
+    })
+    t.equal(value, '{"fixedTupleOfStringsAndNumbers":["string1",42,"string2"]}')
+  } catch (e) {
+    console.log(e)
+    t.fail()
+  }
+})
