data-to-jsonschema-to-ts
Version:
Convert plain javascript objects to JSON Schema and TypeScript typings
191 lines • 6.96 kB
JavaScript
const ANY_TYPE = {
anyOf: [
{ type: "null" },
{ type: "boolean" },
{ type: "number" },
{ type: "string" },
{ type: "object" },
{ type: "array" },
],
};
function intersection(arrays) {
if (arrays.length === 0)
return [];
const [first, ...rest] = arrays;
return first.filter((x) => rest.every((arr) => arr.includes(x)));
}
/**
* Infer a JSON Schema (Draft-07) from an array of sample objects.
*
* Every sample is converted to an object type, then all samples are merged:
* differing types for the same key become `oneOf`, array item types are
* combined via `anyOf`, and `required` is the **intersection** of keys —
* a property is only required if it appears in every sample.
*
* @param data - Sample objects to infer the schema from. Pass an empty array to get an empty object schema.
* @param title - Title for the generated schema. Defaults to `"MySchema"`.
* @param opts - Options controlling `additionalProperties` and an `existingSchema` to merge in.
* @returns A JSON Schema describing the union of shapes seen across `data`.
*/
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) ?? {
type: "object",
properties: {},
additionalProperties: opts.additionalProperties ?? false,
};
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();
objectTypes
.flatMap((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;
}
});
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=schema.js.map