data-to-jsonschema-to-ts
Version:
Convert plain javascript objects to JSON Schema and TypeScript typings
159 lines • 5.84 kB
JavaScript
import { flatMap, intersection } from "lodash-es";
import { ANY_TYPE, } from "./json-schema-types.js";
export function generateJsonSchemaFromData(data, title = "MySchema", opts = {}) {
const initialSchema = {
title,
$schema: "http://json-schema.org/draft-07/schema#",
};
const allObjectTypes = data.map((o) => getObjectType(o, opts));
if (opts.existingSchema) {
allObjectTypes.push({
additionalProperties: opts.existingSchema.additionalProperties,
properties: opts.existingSchema.properties,
required: opts.existingSchema.required,
type: "object",
});
}
const mergedTopLevel = createOneObjectTypeFromManyObjectTypes(allObjectTypes, opts);
return {
...initialSchema,
...mergedTopLevel,
};
}
function isTypePrimitive(type) {
return ["null", "boolean", "number", "string"].includes(type.type);
}
function getUniqueArrayItems(arr) {
const arrItems = new Map();
arr.forEach((i) => {
const iTypeStringified = JSON.stringify(i).split("").sort().join("");
if (!arrItems.has(iTypeStringified)) {
arrItems.set(iTypeStringified, i);
}
});
return Array.from(arrItems.values());
}
function createOneArrayTypeFromManyArrayTypes(arrayTypes, opts) {
if (!arrayTypes.length)
return undefined;
const newArrayType = {
type: "array",
};
const allArraysItemTypes = arrayTypes
.map((arr) => {
if (arr.items && "anyOf" in arr.items && arr.items.anyOf) {
return arr.items.anyOf;
}
})
.filter((t) => t !== undefined)
.flat();
const anyOf = [];
const objectType = createOneObjectTypeFromManyObjectTypes(allArraysItemTypes.filter((t) => t.type === "object"), opts);
const arrType = createOneArrayTypeFromManyArrayTypes(allArraysItemTypes.filter((t) => t.type === "array"), opts);
const primitiveTypes = getUniqueArrayItems(allArraysItemTypes.filter((t) => isTypePrimitive(t)));
const allTypesForKey = [objectType, arrType, ...primitiveTypes].filter((t) => t !== undefined);
anyOf.push(...allTypesForKey);
if (anyOf.length) {
newArrayType.items = { anyOf };
}
return newArrayType;
}
function createOneObjectTypeFromManyObjectTypes(objectTypes, opts) {
if (!objectTypes.length)
return undefined;
const newObjectType = {
type: "object",
properties: {},
additionalProperties: opts?.additionalProperties ?? false,
};
const keyAndTypes = new Map();
flatMap(objectTypes.map((ot) => Object.entries(ot.properties ?? {}))).forEach(([key, type]) => {
const typeList = "oneOf" in type ? type.oneOf : [type];
const existingTypeList = keyAndTypes.get(key) ?? [];
keyAndTypes.set(key, getUniqueArrayItems([...typeList, ...existingTypeList]));
});
Array.from(keyAndTypes.entries()).forEach(([key, types]) => {
const objectTypes = types.filter((t) => t.type === "object");
const objectType = objectTypes.length
? createOneObjectTypeFromManyObjectTypes(objectTypes, opts)
: undefined;
const arrTypes = types.filter((t) => t.type === "array");
const arrType = createOneArrayTypeFromManyArrayTypes(arrTypes, opts);
const primitiveTypes = getUniqueArrayItems(types.filter((t) => isTypePrimitive(t)));
const allTypesForKey = [objectType, arrType, ...primitiveTypes].filter((t) => t !== undefined);
newObjectType.properties ??= {};
if (allTypesForKey.length > 1) {
// @ts-ignore
newObjectType.properties[key] = {
oneOf: allTypesForKey,
};
}
else if (allTypesForKey.length === 1) {
// @ts-ignore
newObjectType.properties[key] = allTypesForKey[0];
}
else {
delete newObjectType.properties;
}
});
// create required array
const sharedKeys = intersection(...objectTypes.map((ot) => ot.required ?? []));
if (sharedKeys.length) {
newObjectType.required = sharedKeys;
}
return newObjectType;
}
function getArrayItems(arr, opts) {
if (arr.length === 0) {
return undefined;
}
const arrTypes = new Map();
arr.forEach((i) => {
const iType = getType(i, opts);
const iTypeStringified = JSON.stringify(iType).split("").sort().join("");
if (!arrTypes.has(iTypeStringified)) {
arrTypes.set(iTypeStringified, iType);
}
});
return { anyOf: Array.from(arrTypes.values()) };
}
function getType(value, opts) {
if (value === null || value === undefined)
return { type: "null" };
if (Array.isArray(value)) {
return {
type: "array",
items: getArrayItems(value, opts),
};
}
if (typeof value === "object") {
return getObjectType(value, opts);
}
return { type: typeof value };
}
function getObjectType(obj, opts) {
if (!obj)
return { type: "object", additionalProperties: ANY_TYPE };
const objType = Object.entries(obj).reduce((acc, [key, value]) => {
acc.properties ??= {};
// @ts-ignore
acc.properties[key] = getType(value);
return acc;
}, {
type: "object",
properties: {},
additionalProperties: opts?.additionalProperties ?? false,
required: Object.keys(obj),
});
if (objType.properties && !Object.keys(objType.properties).length) {
objType.additionalProperties = ANY_TYPE;
}
if ("required" in objType && !objType.required?.length) {
return {
type: "object",
properties: objType.properties,
};
}
return objType;
}
//# sourceMappingURL=json-schema.js.map