UNPKG

data-to-jsonschema-to-ts

Version:

Convert plain javascript objects to JSON Schema and TypeScript typings

191 lines 6.96 kB
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