Created
May 9, 2020 20:29
-
-
Save elisherer/f7661ae7add4fdd1fe56fba055253f86 to your computer and use it in GitHub Desktop.
JSON Schema parsing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const TypeMapping = { | |
bigint: "integer", | |
boolean: "boolean", | |
number: value => (/\.|e/.test(value.toString()) ? "number" : "integer"), | |
string: "string" | |
}; | |
const getType = value => { | |
const type = typeof value; | |
if (typeof TypeMapping[type] === "function") return TypeMapping[type](value); | |
let result = TypeMapping[type]; | |
if (result) return result; | |
if (value === null) return "null"; | |
if (Array.isArray(value)) return "array"; | |
return "object"; | |
}; | |
export const generateSchema = (jsonObject, options, level) => { | |
const schema = {}; | |
if (!level) { | |
schema.$schema = "http://json-schema.org/draft-07/schema#"; | |
level = 0; | |
} | |
schema.type = getType(jsonObject); | |
if (schema.type === "array") { | |
const typeArray = []; | |
jsonObject.forEach( | |
(values, i) => | |
!typeArray.find(t => t.type === getType(values)) && | |
typeArray.push({ index: i, type: getType(values) }) | |
); | |
if (typeArray.length === 1) { | |
schema.items = generateSchema(jsonObject[0], options, level + 1); | |
} else if (typeArray.length > 1) { | |
schema.items = typeArray.map(t => | |
generateSchema(jsonObject[t.index], options, level + 1) | |
); | |
} | |
} | |
if (schema.type === "object") { | |
const required = [], | |
properties = {}; | |
let hasProperties = false; | |
Object.keys(jsonObject).forEach(key => { | |
hasProperties = true; | |
options.required && required.push(key); | |
properties[key] = generateSchema(jsonObject[key], options, level + 1); | |
}); | |
if (hasProperties) schema.properties = properties; | |
if (required.length) schema.required = required; | |
} | |
return schema; | |
}; | |
const getSampleValue = def => { | |
switch (def.type) { | |
case "number": | |
return 0.5; | |
case "integer": | |
return 1; | |
case "boolean": | |
return true; | |
case "string": | |
return def.enum ? def.enum[0] : "string"; | |
case "null": | |
return null; | |
} | |
return undefined; | |
}; | |
const _internalParseSchema = (schema, properties, defined, sample, options) => { | |
if (!properties) return []; | |
return Object.keys(properties).reduce((paths, childKey) => { | |
let child = properties[childKey]; | |
if (child.allOf) { | |
child = Object.assign( | |
child.allOf.reduce((a, c) => Object.assign(a, c), {}), | |
child | |
); | |
} | |
const { $ref, ...childProperties } = child; | |
if ($ref?.startsWith("#/definitions/")) { | |
const definition = $ref.substr($ref.lastIndexOf("/") + 1); | |
if (!defined.includes(definition)) { | |
// prevent recursion of definitions | |
defined.push(definition); | |
child = { | |
...schema.definitions[definition], // load $ref properties | |
...childProperties // child properties override those of the $ref | |
}; | |
} else { | |
// extract it but only with type to prevent further recursion | |
child = { | |
type: childProperties.type || schema.definitions[definition].type | |
}; | |
} | |
} | |
if (child.type === "object") { | |
sample[childKey] = {}; | |
return paths.concat( | |
childKey, | |
_internalParseSchema( | |
schema, | |
child.properties, | |
defined.slice(), | |
sample[childKey], | |
options | |
).map(p => `${childKey}.${p}`) | |
); | |
} | |
if (child.type === "array") { | |
sample[childKey] = []; | |
const arrayPaths = paths.concat(childKey, `${childKey}[]`); | |
if ( | |
!child.items || | |
(Array.isArray(child.items) && child.items.length === 0) | |
) | |
return arrayPaths; | |
return child.items?.properties | |
? arrayPaths.concat( | |
_internalParseSchema( | |
schema, | |
child.items.properties, | |
defined.slice(), | |
sample[childKey], | |
options | |
).map(p => `${childKey}[].${p}`) | |
) | |
: arrayPaths; // TODO: complete this | |
} | |
let sampleValue = getSampleValue(child); | |
if (typeof options.replaceValue === "function") { | |
sampleValue = options.replaceValue(sampleValue); | |
} | |
if (Array.isArray(sample)) { | |
sample.push(sampleValue); | |
} else { | |
sample[childKey] = sampleValue; | |
} | |
return paths.concat(childKey); | |
}, []); | |
}; | |
export const parseSchema = (schema, options = {}) => { | |
if (!schema) return { inputs: [], sample: null }; | |
const sample = {}; | |
const inputs = _internalParseSchema( | |
schema, | |
schema.properties, | |
[], | |
sample, | |
options | |
); | |
return { inputs, sample }; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { generateSchema, parseSchema } from "./jsonschema"; | |
import recursiveSchema from "./recursive.schema"; | |
describe("jsonschema module", () => { | |
test("generateSchema - should return the correct schema for an example (with required)", () => { | |
const schema = generateSchema( | |
{ | |
eli: 2, | |
sherer: true, | |
a: { | |
b: {}, | |
c: 5.4 | |
}, | |
cookies: [ | |
{ | |
eli: 4 | |
} | |
] | |
}, | |
{ required: true } | |
); | |
expect(schema).toMatchSnapshot(); | |
}); | |
test("generateSchema - should return the correct schema for an example (without required)", () => { | |
const schema = generateSchema( | |
{ | |
num: 2, | |
bool: true, | |
obi: { | |
kanobi: {}, | |
float: 5.4 | |
}, | |
null: null, | |
cookies: [ | |
{ | |
four: 4 | |
} | |
], | |
array_of_2_types: ["string", 0], | |
array_untyped: [] | |
}, | |
{ required: false } | |
); | |
expect(schema).toMatchSnapshot(); | |
}); | |
test("parseSchema - should return the correct jsonpaths from schema", () => { | |
const paths = parseSchema(recursiveSchema); | |
expect(paths.inputs).toEqual([ | |
"a", | |
"a.b", | |
"a.b.a", | |
"b", | |
"b.a", | |
"id", | |
"no_props", | |
"array", | |
"array[]", | |
"array[].item", | |
"obj", | |
"obj.nested" | |
]); | |
}); | |
test("parseSchema - no schema", () => { | |
const paths = parseSchema(null); | |
expect(paths).toMatchSnapshot(); | |
}); | |
test("parseSchema - should return the correct sample from schema", () => { | |
const paths = parseSchema(recursiveSchema); | |
expect(paths).toMatchSnapshot(); | |
}); | |
test("parseSchema - test all types", () => { | |
const paths = parseSchema({ | |
type: "object", | |
properties: { | |
id: { type: "integer" }, | |
name: { type: "string" }, | |
age: { type: "number" }, | |
spouse: { type: "object", properties: { id: { type: "integer" } } }, | |
children: { | |
type: "array", | |
items: { type: "object" } | |
}, | |
male: { type: "boolean" }, | |
reserved: { type: "null" }, | |
unknown: {}, | |
untyped: { type: "array", items: [] }, | |
all: { | |
allOf: [{ type: "boolean" }], | |
type: "string" | |
} | |
} | |
}); | |
expect(paths.sample).toMatchSnapshot(); | |
}); | |
test("parseSchema - test replaceValue", () => { | |
const paths = parseSchema( | |
{ | |
type: "object", | |
properties: { | |
id: { type: "integer" }, | |
name: { type: "string" } | |
} | |
}, | |
{ replaceValue: x => `{${x}}` } | |
); | |
expect(paths.sample).toMatchSnapshot(); | |
}); | |
test("parseSchema - string enum", () => { | |
const paths = parseSchema({ | |
type: "object", | |
properties: { | |
name: { type: "string", enum: ["hello", "world"] } | |
} | |
}); | |
expect(paths.sample.name).toBe("hello"); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Jest Snapshot v1, https://goo.gl/fbAQLP | |
exports[`jsonschema module generateSchema - should return the correct schema for an example (with required) 1`] = ` | |
Object { | |
"$schema": "http://json-schema.org/draft-07/schema#", | |
"properties": Object { | |
"a": Object { | |
"properties": Object { | |
"b": Object { | |
"type": "object", | |
}, | |
"c": Object { | |
"type": "number", | |
}, | |
}, | |
"required": Array [ | |
"b", | |
"c", | |
], | |
"type": "object", | |
}, | |
"cookies": Object { | |
"items": Object { | |
"properties": Object { | |
"eli": Object { | |
"type": "integer", | |
}, | |
}, | |
"required": Array [ | |
"eli", | |
], | |
"type": "object", | |
}, | |
"type": "array", | |
}, | |
"eli": Object { | |
"type": "integer", | |
}, | |
"sherer": Object { | |
"type": "boolean", | |
}, | |
}, | |
"required": Array [ | |
"eli", | |
"sherer", | |
"a", | |
"cookies", | |
], | |
"type": "object", | |
} | |
`; | |
exports[`jsonschema module generateSchema - should return the correct schema for an example (without required) 1`] = ` | |
Object { | |
"$schema": "http://json-schema.org/draft-07/schema#", | |
"properties": Object { | |
"array_of_2_types": Object { | |
"items": Array [ | |
Object { | |
"type": "string", | |
}, | |
Object { | |
"type": "integer", | |
}, | |
], | |
"type": "array", | |
}, | |
"array_untyped": Object { | |
"type": "array", | |
}, | |
"bool": Object { | |
"type": "boolean", | |
}, | |
"cookies": Object { | |
"items": Object { | |
"properties": Object { | |
"four": Object { | |
"type": "integer", | |
}, | |
}, | |
"type": "object", | |
}, | |
"type": "array", | |
}, | |
"null": Object { | |
"type": "null", | |
}, | |
"num": Object { | |
"type": "integer", | |
}, | |
"obi": Object { | |
"properties": Object { | |
"float": Object { | |
"type": "number", | |
}, | |
"kanobi": Object { | |
"type": "object", | |
}, | |
}, | |
"type": "object", | |
}, | |
}, | |
"type": "object", | |
} | |
`; | |
exports[`jsonschema module parseSchema - no schema 1`] = ` | |
Object { | |
"inputs": Array [], | |
"sample": null, | |
} | |
`; | |
exports[`jsonschema module parseSchema - should return the correct sample from schema 1`] = ` | |
Object { | |
"inputs": Array [ | |
"a", | |
"a.b", | |
"a.b.a", | |
"b", | |
"b.a", | |
"id", | |
"no_props", | |
"array", | |
"array[]", | |
"array[].item", | |
"obj", | |
"obj.nested", | |
], | |
"sample": Object { | |
"a": Object { | |
"b": Object { | |
"a": Object {}, | |
}, | |
}, | |
"array": Array [ | |
"string", | |
], | |
"b": Object { | |
"a": Object {}, | |
}, | |
"id": "string", | |
"no_props": Object {}, | |
"obj": Object { | |
"nested": "string", | |
}, | |
}, | |
} | |
`; | |
exports[`jsonschema module parseSchema - test all types 1`] = ` | |
Object { | |
"age": 0.5, | |
"all": "string", | |
"children": Array [], | |
"id": 1, | |
"male": true, | |
"name": "string", | |
"reserved": null, | |
"spouse": Object { | |
"id": 1, | |
}, | |
"unknown": undefined, | |
"untyped": Array [], | |
} | |
`; | |
exports[`jsonschema module parseSchema - test replaceValue 1`] = ` | |
Object { | |
"id": "{1}", | |
"name": "{string}", | |
} | |
`; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"$schema": "http://json-schema.org/draft-07/schema#", | |
"definitions": { | |
"BType": { | |
"type": "object", | |
"properties": { | |
"a": { | |
"$ref": "#/definitions/AType" | |
} | |
} | |
}, | |
"AType": { | |
"type": "object", | |
"properties": { | |
"b": { | |
"$ref": "#/definitions/BType" | |
} | |
} | |
} | |
}, | |
"type": "object", | |
"properties": { | |
"a": { | |
"$ref": "#/definitions/AType" | |
}, | |
"b": { | |
"$ref": "#/definitions/BType" | |
}, | |
"id": { | |
"type": "string" | |
}, | |
"no_props": { | |
"type": "object" | |
}, | |
"array": { | |
"type": "array", | |
"items": { | |
"type": "object", | |
"properties": { | |
"item": { "type": "string" } | |
} | |
} | |
}, | |
"obj": { | |
"type": "object", | |
"properties": { | |
"nested": { "type": "string" } | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment