Repository: fastify/fast-json-stringify (JavaScript)

Issue:
Support for array of types in JSON schema
The following is a valid schema, but currently unsupported by fast-json-stringify:
```json
{
    "type": "object",
    "properties": {
      "stringOrNumber": {"type": ["string", "number"], "maxLength": 10}
    }
}
```


Discussion:
Definitely! Would you like to send a PR?
I am already working on it.

We need to conditionally require `ajv` like it is done for `anyOf` in the `hasAnyOf` function. The hard part is to figure out if the schema includes any type arrays.



Patch:
diff --git a/index.js b/index.js
index cf3dc7f2..a61c2b26 100644
--- a/index.js
+++ b/index.js
@@ -90,7 +90,7 @@ function build (schema, options) {
 
   var dependencies = []
   var dependenciesName = []
-  if (hasAnyOf(schema)) {
+  if (hasAnyOf(schema) || hasArrayOfTypes(schema)) {
     dependencies.push(new Ajv())
     dependenciesName.push('ajv')
   }
@@ -113,6 +113,40 @@ function hasAnyOf (schema) {
   return false
 }
 
+function hasArrayOfTypes (schema) {
+  if (Array.isArray(schema.type)) { return true }
+  var i
+
+  if (schema.type === 'object') {
+    if (schema.properties) {
+      var propertyKeys = Object.keys(schema.properties)
+      for (i = 0; i < propertyKeys.length; i++) {
+        if (hasArrayOfTypes(schema.properties[propertyKeys[i]])) {
+          return true
+        }
+      }
+    }
+  } else if (schema.type === 'array') {
+    if (Array.isArray(schema.items)) {
+      for (i = 0; i < schema.items.length; i++) {
+        if (hasArrayOfTypes(schema.items[i])) {
+          return true
+        }
+      }
+    } else if (schema.items) {
+      return hasArrayOfTypes(schema.items)
+    }
+  } else if (Array.isArray(schema.anyOf)) {
+    for (i = 0; i < schema.anyOf.length; i++) {
+      if (hasArrayOfTypes(schema.anyOf[i])) {
+        return true
+      }
+    }
+  }
+
+  return false
+}
+
 function $asNull () {
   return 'null'
 }
@@ -493,32 +527,7 @@ function buildArray (schema, code, name, externalSchema, fullSchema) {
     result = schema.items.reduce((res, item, i) => {
       var accessor = '[i]'
       const tmpRes = nested(laterCode, name, accessor, item, externalSchema, fullSchema, i)
-      var condition = `i === ${i} && `
-      switch (item.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:
-          throw new Error(`${item.type} unsupported`)
-      }
+      var condition = `i === ${i} && ${buildArrayTypeCondition(item.type, accessor)}`
       return {
         code: `${res.code}
         ${i > 0 ? 'else' : ''} if (${condition}) {
@@ -561,6 +570,43 @@ function buildArray (schema, code, name, externalSchema, fullSchema) {
   return code
 }
 
+function buildArrayTypeCondition (type, accessor) {
+  var 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)) {
+        var conditions = type.map((subType) => {
+          return buildArrayTypeCondition(subType, accessor)
+        })
+        condition = `(${conditions.join(' || ')})`
+      } else {
+        throw new Error(`${type} unsupported`)
+      }
+  }
+  return condition
+}
+
 function nested (laterCode, name, key, schema, externalSchema, fullSchema, subKey) {
   var code = ''
   var funcName
@@ -622,7 +668,19 @@ function nested (laterCode, name, key, schema, externalSchema, fullSchema, subKe
       } else throw new Error(`${schema} unsupported`)
       break
     default:
-      throw new Error(`${type} unsupported`)
+      if (Array.isArray(type)) {
+        type.forEach((type, index) => {
+          var tempSchema = {type: type}
+          var nestedResult = nested(laterCode, name, key, tempSchema, externalSchema, fullSchema, subKey)
+          code += `
+            ${index === 0 ? 'if' : 'else if'}(ajv.validate(${require('util').inspect(tempSchema, {depth: null})}, obj${accessor}))
+              ${nestedResult.code}
+          `
+          laterCode = nestedResult.laterCode
+        })
+      } else {
+        throw new Error(`${type} unsupported`)
+      }
   }
 
   return {


Test patch:
diff --git a/test/typesArray.test.js b/test/typesArray.test.js
new file mode 100644
index 00000000..d1baa272
--- /dev/null
+++ b/test/typesArray.test.js
@@ -0,0 +1,184 @@
+'use strict'
+
+const test = require('tap').test
+const build = require('..')
+
+test('simple object with multi-type property', (t) => {
+  t.plan(2)
+
+  const schema = {
+    title: 'simple object with multi-type property',
+    type: 'object',
+    properties: {
+      stringOrNumber: {
+        type: ['string', 'number']
+      }
+    }
+  }
+  const stringify = build(schema)
+
+  try {
+    const value = stringify({
+      stringOrNumber: 'string'
+    })
+    t.is(value, '{"stringOrNumber":"string"}')
+  } catch (e) {
+    t.fail()
+  }
+
+  try {
+    const value = stringify({
+      stringOrNumber: 42
+    })
+    t.is(value, '{"stringOrNumber":42}')
+  } catch (e) {
+    t.fail()
+  }
+})
+
+test('object with array of multiple types', (t) => {
+  t.plan(3)
+
+  const schema = {
+    title: 'object with array of multiple types',
+    type: 'object',
+    properties: {
+      arrayOfStringsAndNumbers: {
+        type: 'array',
+        items: {
+          type: ['string', 'number']
+        }
+      }
+    }
+  }
+  const stringify = build(schema)
+
+  try {
+    const value = stringify({
+      arrayOfStringsAndNumbers: ['string1', 'string2']
+    })
+    t.is(value, '{"arrayOfStringsAndNumbers":["string1","string2"]}')
+  } catch (e) {
+    console.log(e)
+    t.fail()
+  }
+
+  try {
+    const value = stringify({
+      arrayOfStringsAndNumbers: [42, 7]
+    })
+    t.is(value, '{"arrayOfStringsAndNumbers":[42,7]}')
+  } catch (e) {
+    t.fail()
+  }
+
+  try {
+    const value = stringify({
+      arrayOfStringsAndNumbers: ['string1', 42, 7, 'string2']
+    })
+    t.is(value, '{"arrayOfStringsAndNumbers":["string1",42,7,"string2"]}')
+  } catch (e) {
+    t.fail()
+  }
+})
+
+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)
+
+  try {
+    const value = stringify({
+      fixedTupleOfStringsAndNumbers: ['string1', 42, 7]
+    })
+    t.is(value, '{"fixedTupleOfStringsAndNumbers":["string1",42,7]}')
+  } catch (e) {
+    console.log(e)
+    t.fail()
+  }
+
+  try {
+    const value = stringify({
+      fixedTupleOfStringsAndNumbers: ['string1', 42, 'string2']
+    })
+    t.is(value, '{"fixedTupleOfStringsAndNumbers":["string1",42,"string2"]}')
+  } catch (e) {
+    console.log(e)
+    t.fail()
+  }
+})
+
+test('object with anyOf and multiple types', (t) => {
+  t.plan(3)
+
+  const schema = {
+    title: 'object with anyOf and multiple types',
+    type: 'object',
+    properties: {
+      objectOrBoolean: {
+        anyOf: [
+          {
+            type: 'object',
+            properties: {
+              stringOrNumber: {
+                type: ['string', 'number']
+              }
+            }
+          },
+          {
+            type: 'boolean'
+          }
+        ]
+      }
+    }
+  }
+  const stringify = build(schema)
+
+  try {
+    const value = stringify({
+      objectOrBoolean: { stringOrNumber: 'string' }
+    })
+    t.is(value, '{"objectOrBoolean":{"stringOrNumber":"string"}}')
+  } catch (e) {
+    console.log(e)
+    t.fail()
+  }
+
+  try {
+    const value = stringify({
+      objectOrBoolean: { stringOrNumber: 42 }
+    })
+    t.is(value, '{"objectOrBoolean":{"stringOrNumber":42}}')
+  } catch (e) {
+    t.fail()
+  }
+
+  try {
+    const value = stringify({
+      objectOrBoolean: true
+    })
+    t.is(value, '{"objectOrBoolean":true}')
+  } catch (e) {
+    t.fail()
+  }
+})