UNPKG

json-to-pydantic-code-generator

Version:

Generate Pydantic models from JSON. A TypeScript tool that analyzes JSON structures and outputs fully typed Pydantic model code.

625 lines (600 loc) 18.3 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { InvalidIndentationError: () => InvalidIndentationError, InvalidJSONString: () => InvalidJSONString, MalformedJSONError: () => MalformedJSONError, generatePydanticCode: () => generatePydanticCode }); module.exports = __toCommonJS(index_exports); // src/modules/pydantic-code-generator/classes/ListSet.class.ts var ListSet = class extends Set { toString() { return `ListSet { ${[...this].map(String).join(", ")} }`; } }; // src/modules/pydantic-code-generator/classes/TypeSet.class.ts var TypeSet = class extends Set { toString() { return `TypeSet { ${[...this].map(String).join(", ")} }`; } }; // src/modules/pydantic-code-generator/errors/InvalidIndentationError.error.ts var InvalidIndentationError = class extends Error { constructor(message) { super(message); this.name = "InvalidIndentationError"; } }; // src/modules/pydantic-code-generator/errors/InvalidJSONStringError.error.ts var InvalidJSONString = class extends Error { constructor(message) { super(message); this.name = "InvalidJSONStringError"; } }; // src/modules/pydantic-code-generator/consts/PYTHON_RESERVED_KEYWORDS.const.ts var PYTHON_RESERVED_KEYWORDS = /* @__PURE__ */ new Set([ "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", "with", "yield" ]); // src/modules/pydantic-code-generator/functions/setToTypeAnnotation.function.ts function setToTypeAnnotation(s) { if (s.has("int") && s.has("float")) { s.delete("int"); } if (s instanceof ListSet) { if (s.size === 0) { return "List"; } return `List[${setToTypeAnnotation(/* @__PURE__ */ new Set([...s]))}]`; } if (s.size > 1) { if (s.delete("Any")) { return `Optional[${setToTypeAnnotation(s)}]`; } return `Union[${[...s].map((e) => { if (e instanceof ListSet) { return setToTypeAnnotation(e); } return e; }).sort().join(", ")}]`; } const uniqueElement = [...s][0]; if (typeof uniqueElement === "string") { return uniqueElement; } return setToTypeAnnotation(uniqueElement); } // src/modules/pydantic-code-generator/functions/generateClass.function.ts function generateClass(obj, indentation = 4, aliasCamelCase = false, useTabs = false) { obj.attributes.map((e) => { if (PYTHON_RESERVED_KEYWORDS.has(e.name)) { e.alias = e.name; e.name = `${e.name}_`; } }); obj.attributes.map((e) => { if (e.name.match(/^[0-9]/)) { if (!e.alias) { e.alias = e.name; } e.name = `field_${e.name}`; } }); if (aliasCamelCase) { obj.attributes.map((e) => { const newName = e.name.split(/(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/).map((s) => s.toLowerCase()).join("_"); if (!e.alias && e.name !== newName) { e.alias = e.name; } e.name = newName; }); } obj.attributes.map((e) => { if (e.name.match(/[^a-zA-Z0-9_]+/)) { if (!e.alias) { e.alias = e.name; } e.name = e.name.replaceAll(/[^a-zA-Z0-9_]/g, "_"); } let originalName = e.name; let c = 1; while (obj.attributes.filter((el) => el.name === e.name).length > 1) { e.name = `${originalName}_${c}`; c++; } return e; }); const attributes = obj.attributes.length > 0 ? obj.attributes.map((attr) => { let posfix = ""; if (attr.alias) { posfix = ` = Field(${attr.type.has("Any") ? "None" : "..."}, alias='${attr.alias}')`; } else if (attr.type.has("Any") && !(attr.type instanceof ListSet) && attr.type.size > 1) { posfix = " = None"; } return `${useTabs ? " " : " ".repeat(indentation)}${attr.name}: ${setToTypeAnnotation(attr.type)}${posfix}`; }).join("\n") : `${" ".repeat(indentation)}pass`; return `class ${obj.className}(BaseModel): ${attributes}`.trim(); } // src/modules/pydantic-code-generator/utils/utils.module.ts var import_pluralize = __toESM(require("pluralize"), 1); function nonCommonElements(lists) { const allElements = new Set(lists.flat()); const frequency = /* @__PURE__ */ new Map(); allElements.forEach((element) => { const count = lists.reduce( (acc, list) => acc + (list.includes(element) ? 1 : 0), 0 ); frequency.set(element, count); }); return Array.from(frequency.entries()).filter(([_, count]) => count < lists.length).map(([element, _]) => element); } function getClassName(base, reserved = PYTHON_RESERVED_KEYWORDS) { const baseName = base.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-zA-Z0-9\s_]/g, "").split(/[\s_]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(""); let candidate = baseName; const suffix = "Model"; let i = 1; while (reserved.has(candidate)) { if (i > 5) { return `${baseName}Model1`; } candidate = `${baseName}${suffix.slice(0, i)}`; i++; } if (candidate.match(/^[0-9]/)) { candidate = `Class_${candidate}`; } return candidate; } function getNonDuplicateName(newName, existentNames) { let posfix = 1; let renamedName = newName; while (existentNames.includes(renamedName)) { renamedName = `${newName}${posfix}`; posfix++; } return renamedName; } function getArrayClassName(attributeName) { const singularName = import_pluralize.default.singular(attributeName); if (attributeName === singularName) { return `${attributeName}Item`; } return singularName; } function hasType(set, value) { for (const type of set) { if (type instanceof ListSet) { if (hasType(type, value)) { return true; } } else if (type === value) { return true; } } return false; } function addType(set, ...values) { for (const value of values) { if (value instanceof TypeSet) { throw new Error("Add TypeSet is not allowed"); } if (value instanceof ListSet) { const alreadyHasListSet = [...set].find((v) => v instanceof ListSet); if (alreadyHasListSet) { value.forEach((e) => addType(alreadyHasListSet, e)); continue; } } set.add(value); } return set; } function equalTypes(setA, setB) { if (setA instanceof TypeSet && setB instanceof ListSet || setA instanceof ListSet && setB instanceof TypeSet) { return false; } if (setA.size !== setB.size) { return false; } for (const item of setA) { if (typeof item === "string") { if (![...setB].some((i) => i === item)) return false; } else { if (![...setB].some((i) => i instanceof ListSet && equalTypes(i, item))) return false; } } return true; } function replaceType(set, oldType, newType) { if (set.delete(oldType)) { set.add(newType); } for (const item of set) { if (item instanceof ListSet) { replaceType(item, oldType, newType); break; } } } function hasOwnProperties(obj) { return Object.keys(obj).length > 0; } // src/modules/pydantic-code-generator/functions/getType.function.ts function getType(value) { switch (typeof value) { case "string": return "str"; case "number": if (Number.isInteger(value)) { return "int"; } return "float"; case "boolean": return "bool"; case "object": if (value !== null && !hasOwnProperties(value)) { return "Dict[str, Any]"; } default: return "Any"; } } // src/modules/pydantic-code-generator/functions/mergeTypes.function.ts function mergeTypes(oldTypes, typeToAdd) { if (oldTypes instanceof ListSet) { if (typeToAdd instanceof ListSet) { return addType(oldTypes, ...typeToAdd); } return addType(typeToAdd, oldTypes); } if (oldTypes instanceof TypeSet) { if (typeToAdd instanceof TypeSet) { typeToAdd.forEach((e) => addType(oldTypes, e)); return oldTypes; } } return addType(oldTypes, typeToAdd); } // src/modules/pydantic-code-generator/functions/mergeAttributes.function.ts function mergeAttributes(classModel, existingClass) { for (const attr of classModel.attributes) { const existingAttr = existingClass.attributes.find( (a) => a.name === attr.name ); if (existingAttr) { existingAttr.type = mergeTypes(existingAttr.type, attr.type); } else { existingClass.attributes.push(attr); } } } // src/modules/pydantic-code-generator/functions/setOptional.function.ts function setOptional(classes, classModel) { const optionalAttrs = nonCommonElements( classes.filter((c) => c.className === classModel.className).map((e) => e.attributes.map((a) => a.name)) ); for (const attr of classModel.attributes) { if (optionalAttrs.includes(attr.name)) { if (attr.type instanceof ListSet) { attr.type = new TypeSet(["Any", attr.type]); } else { addType(attr.type, "Any"); } } } } // src/modules/pydantic-code-generator/functions/mergeClasses.function.ts function mergeClasses(classes) { const res = []; for (const classModel of classes) { setOptional(classes, classModel); const existingClass = res.find((e) => e.className === classModel.className); if (existingClass) { mergeAttributes(classModel, existingClass); } else { res.push(classModel); } } return res; } // src/modules/pydantic-code-generator/functions/processArray.function.ts function processArray(value, name = "Model", existentClassNames = [], fromObjectArrayJson = false, preferClassReuse = false) { const generatedClassModels = []; const types = new ListSet(); value.forEach((v) => { if (typeof v !== "object") { addType(types, getType(v)); return; } if (Array.isArray(v)) { const res = processArray(v, name, existentClassNames, preferClassReuse); generatedClassModels.push(...res.generatedClassModels); addType(types, res.newAttribute.type); return; } if (v === null) { addType(types, "Any"); return; } const className = getClassName(name); const newName = fromObjectArrayJson ? className : getArrayClassName(className); generatedClassModels.push( ...generateClasses( v, getNonDuplicateName(newName, existentClassNames), existentClassNames, preferClassReuse ) ); addType(types, generatedClassModels.at(-1).className); }); const orderedClassModels = mergeClasses(generatedClassModels).sort((a, b) => { const aDependsOnB = a.attributes.some( (attr) => hasType(attr.type, b.className) ); const bDependsOnA = b.attributes.some( (attr) => hasType(attr.type, a.className) ); if (aDependsOnB && !bDependsOnA) { return 1; } if (bDependsOnA && !aDependsOnB) { return -1; } return 0; }); if (orderedClassModels.length === 1 && orderedClassModels[0].attributes.length === 0) { replaceType(types, orderedClassModels[0].className, "Dict[str, Any]"); return { generatedClassModels: [], newAttribute: { name, type: types } }; } return { generatedClassModels: orderedClassModels, newAttribute: { name, type: types } }; } // src/modules/pydantic-code-generator/functions/reuseClasses.function.ts function reuseClasses(oldClasses, newClasses, newAttribute) { while (newClasses.length > 0) { const newClass = newClasses.shift(); if (newClass) { const coincidentalClass = oldClasses.find( (e) => e.attributes.length === newClass.attributes.length && e.attributes.every( (el) => newClass.attributes.some( (ele) => ele.name === el.name && equalTypes(ele.type, el.type) ) ) ); if (coincidentalClass) { for (const classModel of newClasses) { classModel.attributes.map((e) => { if (hasType(e.type, newClass.className)) { replaceType( e.type, newClass.className, coincidentalClass.className ); } }); } if (newAttribute) { replaceType( newAttribute.type, newClass.className, coincidentalClass.className ); } } else { oldClasses.push(newClass); } } } } // src/modules/pydantic-code-generator/errors/MalformedJSONError.error.ts var MalformedJSONError = class extends Error { constructor(message) { super(message); this.name = "MalformedJSONError"; } }; // src/modules/pydantic-code-generator/functions/generateClasses.function.ts function generateClasses(json, name = "Model", existentClassNames = [], preferClassReuse = false) { const res = []; const obj = { className: name, attributes: [] }; if (typeof json === "object" && Array.isArray(json)) { if (json.some((e) => typeof e !== "object" || Array.isArray(e)) || json.length === 0) { throw new MalformedJSONError( "Input must be an object or an array of objects" ); } return processArray(json, void 0, void 0, true).generatedClassModels; } for (const [key, value] of Object.entries(json)) { const ecn = existentClassNames.concat(res.map((e) => e.className)); ecn.push(name); if (value && typeof value === "object" && !Array.isArray(value) && hasOwnProperties(value)) { const generatedClasses = generateClasses( value, getNonDuplicateName(getClassName(key), ecn), ecn ); if (preferClassReuse) { reuseClasses(res, generatedClasses); } else { res.push(...generatedClasses); } const lastGeneratedClass = res.at(-1); if (lastGeneratedClass) { obj.attributes.push({ name: key, type: new TypeSet([lastGeneratedClass.className]) }); } } else if (Array.isArray(value)) { const processedArray = processArray(value, key, ecn, preferClassReuse); const generatedClassModels = processedArray.generatedClassModels; const newAttribute = processedArray.newAttribute; if (preferClassReuse) { reuseClasses(res, generatedClassModels, newAttribute); } else { res.push(...generatedClassModels); } obj.attributes.push(newAttribute); } else { obj.attributes.push({ name: key, type: new TypeSet([getType(value)]) }); } } res.push(obj); return res; } // src/modules/pydantic-code-generator/functions/getImports.function.ts function getImports(s) { const types = []; if (s.match(/(?<=^|\s)Any(?=\s|$|\])|\[Any\]/g)) { types.push("Any"); } if (s.match(/Dict\[[^\]]+\]/g)) { types.push("Dict"); } if (s.match(/List\[[^\]]+\]/g)) { types.push("List"); } if (s.match(/Optional\[[^\]]+\]/g)) { types.push("Optional"); } if (s.match(/Union\[[^\]]+\]/g)) { types.push("Union"); } const pydanticImports = ["BaseModel"]; if (s.includes("= Field(")) { pydanticImports.push("Field"); } const pydanticImportsStr = `from pydantic import ${pydanticImports.join(", ")}`; if (types.length === 0) { return pydanticImportsStr; } return `from typing import ${types.join(", ")} ${pydanticImportsStr}`; } // src/modules/pydantic-code-generator/pydantic-code-generator.module.ts function addAny(attr) { if (attr.type instanceof ListSet) { attr.type = new TypeSet([attr.type]); } attr.type.add("Any"); } function generatePydanticCode(json, name = "Model", flags = {}) { const { indentation = 4, preferClassReuse = false, forceOptional = "None", aliasCamelCase = false, useTabs = false } = flags; if (indentation < 1) { throw new InvalidIndentationError("Indentation must be greater than 0"); } if (typeof json === "string") { try { json = JSON.parse(json); } catch (error) { throw new InvalidJSONString("The input string is not a valid JSON"); } } const generatedClasses = generateClasses(json, name, [], preferClassReuse); if (forceOptional === "OnlyRootClass") { generatedClasses.at(-1)?.attributes.map((attr) => addAny(attr)); } if (forceOptional === "AllClasses") { generatedClasses.map((c) => c.attributes.map((attr) => addAny(attr))); } const classes = generatedClasses.map((e) => generateClass(e, indentation, aliasCamelCase, useTabs)).join("\n\n\n"); const imports = getImports(classes); return `from __future__ import annotations ${imports} ${classes}`; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { InvalidIndentationError, InvalidJSONString, MalformedJSONError, generatePydanticCode });