Skip to content

Instantly share code, notes, and snippets.

@elisherer
Created May 9, 2020 20:29
Show Gist options
  • Save elisherer/f7661ae7add4fdd1fe56fba055253f86 to your computer and use it in GitHub Desktop.
Save elisherer/f7661ae7add4fdd1fe56fba055253f86 to your computer and use it in GitHub Desktop.
JSON Schema parsing
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 };
};
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");
});
});
// 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}",
}
`;
{
"$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