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
JavaScript
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
});
;