zod-to-x 2.1.0-dev.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,10 @@
3
3
  <em style="font-size: smaller;">Image generated using Canvas AI.</em>
4
4
  </p>
5
5
  <p align="center">
6
- <a href="https://github.com/rroumenov/zod-to-x/releases" target="_blank">
6
+ <a href="https://www.npmjs.com/package/zod-to-x" target="_blank">
7
+ <img src="https://img.shields.io/badge/npm%20-red?style=for-the-badge&logo=npm" alt="npm">
8
+ </a>
9
+ <a href="https://github.com/rroumenov/zod-to-x/releases" target="_blank" style="margin-left: 10px;">
7
10
  <img src="https://img.shields.io/badge/View%20Changelog-brightgreen?style=for-the-badge" alt="View Changelog">
8
11
  </a>
9
12
  <a href="https://playcode.io/2277071" target="_blank" style="margin-left: 10px;">
@@ -20,6 +23,14 @@
20
23
 
21
24
  <span style="color: red;">**Important Announcement:**</span> `zod-to-x@2.0.0` has been released, introducing migration to Zod V4. At this stage, only the existent behavior has been migrated, while new features like Literal Templates are still under analysis. Only the complete Zod V4 version will be supported, **not v4-mini**. Additionally, `zod-to-x@1.X.Y` will continue to be maintained for Zod V3, and any new transpilation languages will also be supported in version 1.
22
25
 
26
+ <p align="center">
27
+ <strong>Supported Languages:</strong><br><br>
28
+ <img src="https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white" alt="TypeScript">
29
+ <img src="https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white" alt="Python">
30
+ <img src="https://img.shields.io/badge/C++-00599C?style=for-the-badge&logo=cplusplus&logoColor=white" alt="C++">
31
+ <img src="https://img.shields.io/badge/Protobuf-4285F4?style=for-the-badge&logo=google&logoColor=white" alt="Protobuf">
32
+ </p>
33
+
23
34
 
24
35
 
25
36
  ## Table of contents
@@ -32,10 +43,11 @@
32
43
  - [Layered modeling](#layered-modeling)
33
44
  - [Usage example](#usage-example)
34
45
  - [Custom layers](#custom-layers)
35
- - [Generic types](#generic-types) <sup>*(new)*</sup>
46
+ - [Generic types](#generic-types)
36
47
  - [Currently supported output languages](#currently-supported-output-languages)
37
48
  - [Typescript](#1-typescript)
38
- - [C++](#2-c)
49
+ - [Python](#2-python) <sup>*(new)*</sup>
50
+ - [C++](#3-c)
39
51
  - [Additional utils](#additional-utils)
40
52
  - [Protobuf V3 generation](#2-zod2protov3)
41
53
  - [Mapping of supported Zod Types by Language](#mapping-of-supported-zod-types-by-langauge)
@@ -50,7 +62,7 @@ Managing data consistency across multiple layers and languages is a common chall
50
62
  Define your data structures in one place using the powerful [`@zod`](https://github.com/colinhacks/zod) library. This eliminates redundancy, reduces inconsistencies, and simplifies maintenance across your entire codebase, all while allowing you to continue leveraging any npm package in the [`@zod`](https://github.com/colinhacks/zod) ecosystem.
51
63
 
52
64
  2. **Multi-Language Compatibility**
53
- Generate data models for TypeScript, Protobuf V3 and C++ (with languages like Golang on the roadmap). No more manually rewriting models for different platforms.
65
+ Generate data models for TypeScript, Python (Pydantic), C++, and Protobuf V3 (with languages like Golang on the roadmap). No more manually rewriting models for different platforms.
54
66
 
55
67
  3. **Enhanced Productivity**
56
68
  Automate the transpilation of data models to save time, reduce errors, and let your team focus on business logic instead of boilerplate code.
@@ -600,7 +612,13 @@ Common options:
600
612
  - **keepKeys**: Specifies whether property names should follow the TypeScript naming convention (false) or remain as originally defined (true). The default is `false`.
601
613
  - [Examples](https://github.com/rroumenov/zod-to-x/blob/main/test/test_zod2ts)
602
614
 
603
- ### 2) C++
615
+ ### 2) Python
616
+ `Pydantic` is used for data validation and serialization/deserialization. Generates Pydantic BaseModel classes with full type hints.
617
+ - Options:
618
+ - **keepKeys**: Specifies whether property names should follow the Python naming convention (false) or remain as originally defined (true). The default is `false`.
619
+ - [Examples](https://github.com/rroumenov/zod-to-x/blob/main/test/test_zod2py)
620
+
621
+ ### 3) C++
604
622
  `Nlohmann` dependency is used for data serialization/deserialization. For *C++11*, `Boost` dependency is used. For *C++17* or newer, standard libraries are used.
605
623
  - Options:
606
624
  - **includeNulls**: When serializing, include all values even if `null`. Defaults to `false`.
@@ -64,11 +64,13 @@ export declare class Zod2Ast {
64
64
  */
65
65
  private _getEnumValues;
66
66
  /**
67
- * Intersects the properties of two AST nodes and returns the combined properties.
67
+ * Intersects the properties of two Zod object schemas and returns the combined properties.
68
+ * Processes the Zod shapes directly instead of looking up cached AST nodes, ensuring that
69
+ * instantiated generic types (from useGenericType) are correctly resolved.
68
70
  *
69
- * @param left - The left AST definition to intersect.
70
- * @param right - The right AST definition to intersect.
71
- * @returns An object containing the combined properties of the left and right AST nodes.
71
+ * @param left - The left Zod object schema.
72
+ * @param right - The right Zod object schema.
73
+ * @returns An object containing the combined properties of the left and right schemas.
72
74
  */
73
75
  private _intersectAstNodes;
74
76
  /**
@@ -138,18 +138,30 @@ class Zod2Ast {
138
138
  });
139
139
  }
140
140
  /**
141
- * Intersects the properties of two AST nodes and returns the combined properties.
141
+ * Intersects the properties of two Zod object schemas and returns the combined properties.
142
+ * Processes the Zod shapes directly instead of looking up cached AST nodes, ensuring that
143
+ * instantiated generic types (from useGenericType) are correctly resolved.
142
144
  *
143
- * @param left - The left AST definition to intersect.
144
- * @param right - The right AST definition to intersect.
145
- * @returns An object containing the combined properties of the left and right AST nodes.
145
+ * @param left - The left Zod object schema.
146
+ * @param right - The right Zod object schema.
147
+ * @returns An object containing the combined properties of the left and right schemas.
146
148
  */
147
149
  _intersectAstNodes(left, right) {
148
- const leftData = this.nodes.get(left.name);
149
- const rightData = this.nodes.get(right.name);
150
- return {
151
- properties: Object.assign(Object.assign({}, leftData.properties), rightData.properties),
152
- };
150
+ const properties = {};
151
+ for (const schema of [left, right]) {
152
+ const shape = schema.def.shape;
153
+ for (const key in shape) {
154
+ if (zod_helpers_1.ZodHelpers.isZodPromise(shape[key]) &&
155
+ zod_helpers_1.ZodHelpers.isZod2XGeneric(shape[key])) {
156
+ const templateKey = shape[key].unwrap().def.values[0];
157
+ properties[key] = new core_1.ASTGenericType(templateKey);
158
+ }
159
+ else {
160
+ properties[key] = this._zodToAST(shape[key]);
161
+ }
162
+ }
163
+ }
164
+ return { properties };
153
165
  }
154
166
  /**
155
167
  * Merges multiple AST definitions into a single AST object containing combined properties.
@@ -425,7 +437,7 @@ class Zod2Ast {
425
437
  }
426
438
  }
427
439
  else {
428
- const intersectedProperties = this._intersectAstNodes(item.left, item.right);
440
+ const intersectedProperties = this._intersectAstNodes(def.left, def.right);
429
441
  item.newObject = new core_1.ASTObject({
430
442
  name,
431
443
  properties: intersectedProperties.properties,
@@ -43,5 +43,6 @@ export declare class Zod2XModel {
43
43
  transpile(target: Target<"Zod2Cpp">, opt?: TargetOpt<"Zod2Cpp">, astOpt?: AstOpt): string;
44
44
  transpile(target: Target<"Zod2Cpp17">, opt?: TargetOpt<"Zod2Cpp17">, astOpt?: AstOpt): string;
45
45
  transpile(target: Target<"Zod2Ts">, opt?: TargetOpt<"Zod2Ts">, astOpt?: AstOpt): string;
46
+ transpile(target: Target<"Zod2Py">, opt?: TargetOpt<"Zod2Py">, astOpt?: AstOpt): string;
46
47
  }
47
48
  export {};
@@ -1,2 +1,3 @@
1
1
  export { Zod2Ts } from "./typescript/runner";
2
2
  export { Zod2Cpp, Zod2Cpp17 } from "./cpp/runner";
3
+ export { Zod2Py } from "./python/runner";
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Zod2Cpp17 = exports.Zod2Cpp = exports.Zod2Ts = void 0;
3
+ exports.Zod2Py = exports.Zod2Cpp17 = exports.Zod2Cpp = exports.Zod2Ts = void 0;
4
4
  var runner_1 = require("./typescript/runner");
5
5
  Object.defineProperty(exports, "Zod2Ts", { enumerable: true, get: function () { return runner_1.Zod2Ts; } });
6
6
  var runner_2 = require("./cpp/runner");
7
7
  Object.defineProperty(exports, "Zod2Cpp", { enumerable: true, get: function () { return runner_2.Zod2Cpp; } });
8
8
  Object.defineProperty(exports, "Zod2Cpp17", { enumerable: true, get: function () { return runner_2.Zod2Cpp17; } });
9
+ var runner_3 = require("./python/runner");
10
+ Object.defineProperty(exports, "Zod2Py", { enumerable: true, get: function () { return runner_3.Zod2Py; } });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @description Python library imports for Pydantic
3
+ * @returns
4
+ */
5
+ export declare function getLibs(): {
6
+ baseModel: string;
7
+ fieldImport: string;
8
+ aliasGenerator: string;
9
+ annotatedType: string;
10
+ typeAliasType: string;
11
+ genericType: string;
12
+ typeVarType: string;
13
+ enumType: string;
14
+ anyType: string;
15
+ listType: string;
16
+ dictType: string;
17
+ setType: string;
18
+ tupleType: string;
19
+ unionType: string;
20
+ optionalType: string;
21
+ literalType: string;
22
+ datetimeType: string;
23
+ };
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getLibs = getLibs;
4
+ /**
5
+ * @description Python library imports for Pydantic
6
+ * @returns
7
+ */
8
+ function getLibs() {
9
+ return {
10
+ baseModel: "from pydantic import BaseModel, ConfigDict",
11
+ fieldImport: "from pydantic import Field",
12
+ aliasGenerator: "from pydantic.alias_generators import to_camel",
13
+ annotatedType: "from typing import Annotated",
14
+ typeAliasType: "from typing import TypeAlias",
15
+ genericType: "from typing import Generic",
16
+ typeVarType: "from typing import TypeVar",
17
+ enumType: "from enum import Enum",
18
+ anyType: "from typing import Any",
19
+ listType: "from typing import List",
20
+ dictType: "from typing import Dict",
21
+ setType: "from typing import Set",
22
+ tupleType: "from typing import Tuple",
23
+ unionType: "from typing import Union",
24
+ optionalType: "from typing import Optional",
25
+ literalType: "from typing import Literal",
26
+ datetimeType: "from datetime import datetime",
27
+ };
28
+ }
@@ -0,0 +1,9 @@
1
+ import { IZodToXOpt } from "../../core";
2
+ export interface IZod2PyOpt extends Omit<IZodToXOpt, "indent"> {
3
+ /**
4
+ * By default (false), structure/class property names are converted according to the target
5
+ * language's naming conventions. If set to true, the original property names are preserved.
6
+ */
7
+ keepKeys?: boolean;
8
+ }
9
+ export declare const defaultOpts: IZod2PyOpt;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defaultOpts = void 0;
4
+ exports.defaultOpts = {
5
+ includeComments: true,
6
+ useImports: true,
7
+ keepKeys: false,
8
+ };
@@ -0,0 +1,116 @@
1
+ import { ASTAliasedTypes, ASTEnum, ASTIntersection, ASTNode, ASTObject, ASTUnion, Zod2X } from "../../core";
2
+ import { IZod2PyOpt } from "./options";
3
+ export declare class Zod2Py extends Zod2X<IZod2PyOpt> {
4
+ protected readonly commentKey = "#";
5
+ protected lib: {
6
+ baseModel: string;
7
+ fieldImport: string;
8
+ aliasGenerator: string;
9
+ annotatedType: string;
10
+ typeAliasType: string;
11
+ genericType: string;
12
+ typeVarType: string;
13
+ enumType: string;
14
+ anyType: string;
15
+ listType: string;
16
+ dictType: string;
17
+ setType: string;
18
+ tupleType: string;
19
+ unionType: string;
20
+ optionalType: string;
21
+ literalType: string;
22
+ datetimeType: string;
23
+ };
24
+ private baseSchemaAdded;
25
+ private typeVars;
26
+ constructor(opt?: IZod2PyOpt);
27
+ protected runAfter(): void;
28
+ protected runBefore(): void;
29
+ /**
30
+ * Adds BaseSchema class definition if not already added.
31
+ * This is the base class for all Pydantic models with shared configuration.
32
+ */
33
+ private _addBaseSchema;
34
+ /**
35
+ * Declares TypeVars that haven't been declared yet.
36
+ * Adds them right before their first usage.
37
+ * Ex: T = TypeVar('T')
38
+ */
39
+ private _declareNewTypeVars;
40
+ /**
41
+ * Consolidates multiline imports from the same module and sorts them alphabetically.
42
+ * Modifies this.imports Set to contain consolidated import statements.
43
+ */
44
+ private _consolidateImports;
45
+ protected addImportFromFile(filename: string, namespace: string): string;
46
+ protected getTypeFromExternalNamespace(namespace: string, typeName: string): string;
47
+ protected addExtendedType(name: string, parentNamespace: string, aliasOf: string, opt?: {
48
+ type?: "union" | "d-union" | "alias";
49
+ isInternal?: boolean;
50
+ templates?: string;
51
+ isClass?: boolean;
52
+ }): void;
53
+ protected getGenericTemplatesTranslation(data: ASTNode): string | undefined;
54
+ protected checkExtendedTypeInclusion(data: ASTNode, type?: "alias" | "union" | "d-union"): boolean;
55
+ protected getAnyType: () => string;
56
+ protected getBooleanType: () => string;
57
+ protected getDateType: () => string;
58
+ /** Ex: Set[TypeA] */
59
+ protected getSetType: (itemType: string) => string;
60
+ protected getStringType: () => string;
61
+ /** Ex: Tuple[TypeA, TypeB] */
62
+ protected getTupleType: (itemsType: string[]) => string;
63
+ /** Ex: Union[TypeA, TypeB] */
64
+ protected getUnionType: (itemsType: string[]) => string;
65
+ /** Ex: TypeA & TypeB -> intersection handling */
66
+ protected getIntersectionType: (itemsType: string[]) => string;
67
+ /** Ex: int or float depending on isInt flag */
68
+ protected getNumberType: (isInt: boolean, range: {
69
+ min?: number;
70
+ max?: number;
71
+ }) => string;
72
+ /** Ex: List[List[TypeA]] */
73
+ protected getArrayType(arrayType: string, arrayDeep: number): string;
74
+ /** Ex: Literal["value"] or Literal[true] or EnumName.ENUM_VALUE */
75
+ protected getLiteralStringType(value: string | number | boolean, parentEnumNameKey?: [string, string]): string | number;
76
+ /** Ex: Dict[TypeA, TypeB] */
77
+ protected getMapType(keyType: string, valueType: string): string;
78
+ /** Ex: Dict[TypeA, TypeB] */
79
+ protected getRecordType(keyType: string, valueType: string): string;
80
+ protected transpileAliasedType(data: ASTAliasedTypes): void;
81
+ /** Ex:
82
+ * class MyEnum(str, Enum):
83
+ * ITEM_KEY1 = "ItemValue1"
84
+ * ITEM_KEY2 = "ItemValue2"
85
+ *
86
+ * # Or for mixed types:
87
+ * class MyEnum(Enum):
88
+ * ITEM_KEY1 = 1
89
+ * ITEM_KEY2 = "ItemValue2"
90
+ */
91
+ protected transpileEnum(data: ASTEnum): void;
92
+ protected transpileIntersection(data: ASTIntersection): void;
93
+ protected transpileStruct(data: ASTObject): void;
94
+ protected transpileUnion(data: ASTUnion): void;
95
+ /**
96
+ * Creates a wrapper class for a union type.
97
+ * Python/Pydantic needs this for proper serialization/deserialization of unions.
98
+ * Ex:
99
+ * class UnionItemWrapper(BaseSchema):
100
+ * data: UnionItem
101
+ */
102
+ private _createUnionWrapper;
103
+ /** Ex:
104
+ * class MyStruct(BaseSchema):
105
+ * att1: TypeA
106
+ * att2: Optional[TypeB] = None
107
+ *
108
+ * # Or with generics:
109
+ * class MyGenericStruct(BaseSchema, Generic[T]):
110
+ * data: T
111
+ * */
112
+ private _transpileStructAsClass;
113
+ /** For Class attributes.
114
+ * Ex: attribute1: Optional[TypeA] = None */
115
+ private _transpileMember;
116
+ }
@@ -0,0 +1,397 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Zod2Py = void 0;
7
+ const case_1 = __importDefault(require("case"));
8
+ const core_1 = require("../../core");
9
+ const options_1 = require("./options");
10
+ const libs_1 = require("./libs");
11
+ class Zod2Py extends core_1.Zod2X {
12
+ constructor(opt = {}) {
13
+ super(Object.assign(Object.assign({}, options_1.defaultOpts), opt));
14
+ this.commentKey = "#";
15
+ this.baseSchemaAdded = false;
16
+ this.typeVars = new Set();
17
+ this.getAnyType = () => {
18
+ this.imports.add(this.lib.anyType);
19
+ return "Any";
20
+ };
21
+ this.getBooleanType = () => "bool";
22
+ this.getDateType = () => {
23
+ this.imports.add(this.lib.datetimeType);
24
+ return "datetime";
25
+ };
26
+ /** Ex: Set[TypeA] */
27
+ this.getSetType = (itemType) => {
28
+ this.imports.add(this.lib.setType);
29
+ return `Set[${itemType}]`;
30
+ };
31
+ this.getStringType = () => "str";
32
+ /** Ex: Tuple[TypeA, TypeB] */
33
+ this.getTupleType = (itemsType) => {
34
+ this.imports.add(this.lib.tupleType);
35
+ return `Tuple[${itemsType.join(", ")}]`;
36
+ };
37
+ /** Ex: Union[TypeA, TypeB] */
38
+ this.getUnionType = (itemsType) => {
39
+ this.imports.add(this.lib.unionType);
40
+ return `Union[${itemsType.join(", ")}]`;
41
+ };
42
+ /** Ex: TypeA & TypeB -> intersection handling */
43
+ this.getIntersectionType = (itemsType) => {
44
+ // Python doesn't have intersection types, we'll create a new class
45
+ return itemsType.join(" & "); // This will be handled in transpileIntersection
46
+ };
47
+ /** Ex: int or float depending on isInt flag */
48
+ this.getNumberType = (isInt, range) => {
49
+ return isInt ? "int" : "float";
50
+ };
51
+ this.lib = (0, libs_1.getLibs)();
52
+ }
53
+ runAfter() {
54
+ this._consolidateImports();
55
+ }
56
+ runBefore() { }
57
+ /**
58
+ * Adds BaseSchema class definition if not already added.
59
+ * This is the base class for all Pydantic models with shared configuration.
60
+ */
61
+ _addBaseSchema() {
62
+ if (this.baseSchemaAdded)
63
+ return;
64
+ this.baseSchemaAdded = true;
65
+ this.imports.add(this.lib.baseModel);
66
+ if (this.opt.keepKeys !== true) {
67
+ this.imports.add(this.lib.aliasGenerator);
68
+ }
69
+ this.push0("class BaseSchema(BaseModel):");
70
+ this.push1("model_config = ConfigDict(");
71
+ if (this.opt.keepKeys !== true) {
72
+ this.push2("alias_generator=to_camel,");
73
+ this.push2("serialize_by_alias=True,");
74
+ this.push2("populate_by_name=True,");
75
+ }
76
+ this.push2("use_enum_values=True");
77
+ this.push1(")");
78
+ this.push0("");
79
+ }
80
+ /**
81
+ * Declares TypeVars that haven't been declared yet.
82
+ * Adds them right before their first usage.
83
+ * Ex: T = TypeVar('T')
84
+ */
85
+ _declareNewTypeVars(templates) {
86
+ const newTypeVars = Array.from(templates).filter((t) => !this.typeVars.has(t));
87
+ if (newTypeVars.length === 0)
88
+ return;
89
+ this.imports.add(this.lib.typeVarType);
90
+ newTypeVars.forEach((typeVar) => {
91
+ this.push0(`${typeVar} = TypeVar('${typeVar}')`);
92
+ this.typeVars.add(typeVar);
93
+ });
94
+ this.push0("");
95
+ }
96
+ /**
97
+ * Consolidates multiline imports from the same module and sorts them alphabetically.
98
+ * Modifies this.imports Set to contain consolidated import statements.
99
+ */
100
+ _consolidateImports() {
101
+ const importGroups = new Map();
102
+ const simpleImports = new Set();
103
+ // Group imports by module
104
+ this.imports.forEach((importStatement) => {
105
+ if (importStatement.startsWith("from ") && importStatement.includes(" import ")) {
106
+ // Parse "from module import item" format
107
+ const match = importStatement.match(/^from\s+(\S+)\s+import\s+(.+)$/);
108
+ if (match) {
109
+ const module = match[1];
110
+ const items = match[2].split(",").map((item) => item.trim());
111
+ if (!importGroups.has(module)) {
112
+ importGroups.set(module, new Set());
113
+ }
114
+ items.forEach((item) => {
115
+ if (item) {
116
+ importGroups.get(module).add(item);
117
+ }
118
+ });
119
+ }
120
+ }
121
+ else if (importStatement.startsWith("import ")) {
122
+ // Simple import statement
123
+ simpleImports.add(importStatement);
124
+ }
125
+ });
126
+ // Clear existing imports
127
+ this.imports.clear();
128
+ // Add consolidated "from" imports back to this.imports (sorted by module)
129
+ const sortedModules = Array.from(importGroups.keys()).sort();
130
+ sortedModules.forEach((module) => {
131
+ const items = Array.from(importGroups.get(module)).sort();
132
+ if (items.length === 1) {
133
+ this.imports.add(`from ${module} import ${items[0]}`);
134
+ }
135
+ else {
136
+ this.imports.add(`from ${module} import ${items.join(", ")}`);
137
+ }
138
+ });
139
+ // Add simple imports back to this.imports (sorted)
140
+ const sortedSimpleImports = Array.from(simpleImports).sort();
141
+ sortedSimpleImports.forEach((imp) => {
142
+ this.imports.add(imp);
143
+ });
144
+ }
145
+ addImportFromFile(filename, namespace) {
146
+ const moduleName = filename.endsWith(".py") ? filename.slice(0, -3) : filename;
147
+ return `import ${moduleName} as ${namespace}`;
148
+ }
149
+ getTypeFromExternalNamespace(namespace, typeName) {
150
+ return `${namespace}.${typeName}`;
151
+ }
152
+ addExtendedType(name, parentNamespace, aliasOf, opt) {
153
+ var _a;
154
+ const extendedType = (opt === null || opt === void 0 ? void 0 : opt.isInternal)
155
+ ? aliasOf
156
+ : this.getTypeFromExternalNamespace(parentNamespace, aliasOf);
157
+ const templates = (_a = opt === null || opt === void 0 ? void 0 : opt.templates) !== null && _a !== void 0 ? _a : "";
158
+ if (opt === null || opt === void 0 ? void 0 : opt.isClass) {
159
+ // For classes (ASTObject, ASTIntersection), use inheritance with templates
160
+ this.push0(`class ${name}(${extendedType}${templates}): ...\n`);
161
+ }
162
+ else {
163
+ // For type aliases (primitives, unions, etc.), use TypeAlias
164
+ this.imports.add(this.lib.typeAliasType);
165
+ this.push0(`${name}: TypeAlias = ${extendedType}${templates}\n`);
166
+ }
167
+ }
168
+ getGenericTemplatesTranslation(data) {
169
+ if ((data instanceof core_1.ASTObject || data instanceof core_1.ASTDefinition) &&
170
+ data.templatesTranslation.length > 0) {
171
+ return ("[" +
172
+ data.templatesTranslation
173
+ .map((t) => {
174
+ if (this.isExternalTypeImport(t)) {
175
+ this.addExternalTypeImport(t);
176
+ return this.getTypeFromExternalNamespace(t.parentNamespace, t.aliasOf);
177
+ }
178
+ else {
179
+ return t.aliasOf;
180
+ }
181
+ })
182
+ .join(", ") +
183
+ "]");
184
+ }
185
+ }
186
+ checkExtendedTypeInclusion(data, type) {
187
+ // Determine if the aliased type is a class (ASTObject or ASTIntersection with newObject)
188
+ const isClass = data instanceof core_1.ASTObject ||
189
+ (data instanceof core_1.ASTIntersection && data.newObject !== undefined);
190
+ if (this.isExternalTypeImport(data)) {
191
+ if (data.aliasOf) {
192
+ this.addExtendedType(data.name, data.parentNamespace, data.aliasOf, {
193
+ type,
194
+ templates: this.getGenericTemplatesTranslation(data),
195
+ isClass,
196
+ });
197
+ this.addExternalTypeImport(data);
198
+ }
199
+ return true;
200
+ }
201
+ else if (data.aliasOf) {
202
+ this.addExtendedType(data.name, data.parentNamespace, data.aliasOf, {
203
+ type,
204
+ isInternal: true,
205
+ templates: this.getGenericTemplatesTranslation(data),
206
+ isClass,
207
+ });
208
+ return true;
209
+ }
210
+ return false;
211
+ }
212
+ /** Ex: List[List[TypeA]] */
213
+ getArrayType(arrayType, arrayDeep) {
214
+ this.imports.add(this.lib.listType);
215
+ let output = `List[${arrayType}]`;
216
+ for (let i = 0; i < arrayDeep - 1; i++) {
217
+ output = `List[${output}]`;
218
+ }
219
+ return output;
220
+ }
221
+ /** Ex: Literal["value"] or Literal[true] or EnumName.ENUM_VALUE */
222
+ getLiteralStringType(value, parentEnumNameKey) {
223
+ if (!parentEnumNameKey)
224
+ this.imports.add(this.lib.literalType);
225
+ return ("Literal[" +
226
+ (parentEnumNameKey
227
+ ? `${parentEnumNameKey[0]}.${case_1.default.constant(case_1.default.snake(parentEnumNameKey[1]))}`
228
+ : typeof value === "boolean"
229
+ ? case_1.default.capital(value.toString())
230
+ : `${isNaN(Number(value)) ? `"${value}"` : value}`) +
231
+ "]");
232
+ }
233
+ /** Ex: Dict[TypeA, TypeB] */
234
+ getMapType(keyType, valueType) {
235
+ this.imports.add(this.lib.dictType);
236
+ return `Dict[${keyType}, ${valueType}]`;
237
+ }
238
+ /** Ex: Dict[TypeA, TypeB] */
239
+ getRecordType(keyType, valueType) {
240
+ this.imports.add(this.lib.dictType);
241
+ return `Dict[${keyType}, ${valueType}]`;
242
+ }
243
+ transpileAliasedType(data) {
244
+ if (this.checkExtendedTypeInclusion(data, "alias")) {
245
+ return;
246
+ }
247
+ let extendedType = undefined;
248
+ this.addComment(data.description);
249
+ if (data instanceof core_1.ASTArray) {
250
+ extendedType = this.getAttributeType(data.item);
251
+ }
252
+ else {
253
+ extendedType = this.getAttributeType(data);
254
+ }
255
+ if (extendedType !== undefined) {
256
+ this.imports.add(this.lib.typeAliasType);
257
+ this.push0(`${data.name}: TypeAlias = ${extendedType}\n`);
258
+ }
259
+ }
260
+ /** Ex:
261
+ * class MyEnum(str, Enum):
262
+ * ITEM_KEY1 = "ItemValue1"
263
+ * ITEM_KEY2 = "ItemValue2"
264
+ *
265
+ * # Or for mixed types:
266
+ * class MyEnum(Enum):
267
+ * ITEM_KEY1 = 1
268
+ * ITEM_KEY2 = "ItemValue2"
269
+ */
270
+ transpileEnum(data) {
271
+ if (this.checkExtendedTypeInclusion(data, "alias")) {
272
+ return;
273
+ }
274
+ this.imports.add(this.lib.enumType);
275
+ this.addComment(data.description);
276
+ // Check if all values are strings
277
+ const allStrings = data.values.every(([, value]) => typeof value === "string");
278
+ const enumParent = allStrings ? "(str, Enum)" : "(Enum)";
279
+ this.push0(`class ${data.name}${enumParent}:`);
280
+ data.values.forEach(([key, value]) => {
281
+ const keyName = case_1.default.constant(case_1.default.snake(key));
282
+ const enumValue = typeof value === "string" ? `"${value}"` : `${value}`;
283
+ this.push1(`${keyName} = ${enumValue}`);
284
+ });
285
+ this.push0("");
286
+ }
287
+ transpileIntersection(data) {
288
+ if (this.checkExtendedTypeInclusion(data)) {
289
+ return;
290
+ }
291
+ this._addBaseSchema();
292
+ this.addComment(data.description);
293
+ // Use multiple inheritance like C++ for intersections
294
+ const leftType = this.getAttributeType(data.left);
295
+ const rightType = this.getAttributeType(data.right);
296
+ this.push0(`class ${data.name}(${leftType}, ${rightType}): ...\n`);
297
+ }
298
+ transpileStruct(data) {
299
+ if (this.checkExtendedTypeInclusion(data)) {
300
+ return;
301
+ }
302
+ this._addBaseSchema();
303
+ if (data.templates.size > 0) {
304
+ this._declareNewTypeVars(data.templates);
305
+ }
306
+ this.addComment(data.description);
307
+ this._transpileStructAsClass(data);
308
+ }
309
+ transpileUnion(data) {
310
+ if (this.checkExtendedTypeInclusion(data, data.discriminantKey === undefined ? "union" : "d-union")) {
311
+ return;
312
+ }
313
+ // Python uses Union type aliases (like C++), not merged classes
314
+ this._addBaseSchema();
315
+ this.addComment(data.description);
316
+ const attributesTypes = data.options.map(this.getAttributeType.bind(this));
317
+ // For discriminated unions, use Annotated with Field discriminator
318
+ if (data.discriminantKey) {
319
+ this.imports.add(this.lib.annotatedType);
320
+ this.imports.add(this.lib.fieldImport);
321
+ this.push0(`${data.name} = Annotated[`);
322
+ this.push1(`${this.getUnionType(attributesTypes)},`);
323
+ this.push1(`Field(discriminator='${data.discriminantKey}')`);
324
+ this.push0(`]\n`);
325
+ }
326
+ else {
327
+ this.push0(`${data.name} = ${this.getUnionType(attributesTypes)}\n`);
328
+ }
329
+ this._createUnionWrapper(data.name);
330
+ }
331
+ /**
332
+ * Creates a wrapper class for a union type.
333
+ * Python/Pydantic needs this for proper serialization/deserialization of unions.
334
+ * Ex:
335
+ * class UnionItemWrapper(BaseSchema):
336
+ * data: UnionItem
337
+ */
338
+ _createUnionWrapper(unionName) {
339
+ const wrapperName = `${unionName}Wrapper`;
340
+ this.push0(`class ${wrapperName}(BaseSchema):`);
341
+ this.push1(`data: ${unionName}`);
342
+ this.push0("");
343
+ }
344
+ /** Ex:
345
+ * class MyStruct(BaseSchema):
346
+ * att1: TypeA
347
+ * att2: Optional[TypeB] = None
348
+ *
349
+ * # Or with generics:
350
+ * class MyGenericStruct(BaseSchema, Generic[T]):
351
+ * data: T
352
+ * */
353
+ _transpileStructAsClass(data) {
354
+ // Handle generic templates
355
+ let baseClasses = "BaseSchema";
356
+ if (data.templates.size > 0) {
357
+ const templates = Array.from(data.templates);
358
+ this.imports.add(this.lib.genericType);
359
+ baseClasses += `, Generic[${templates.join(", ")}]`;
360
+ }
361
+ this.push0(`class ${data.name}(${baseClasses}):`);
362
+ const hasProperties = Object.keys(data.properties).length > 0;
363
+ if (!hasProperties) {
364
+ this.push1("pass");
365
+ this.push0("");
366
+ return;
367
+ }
368
+ // Generate fields
369
+ for (const [key, value] of Object.entries(data.properties)) {
370
+ const keyName = this.opt.keepKeys === true ? key : case_1.default.snake(key);
371
+ this._transpileMember(keyName, value);
372
+ }
373
+ this.push0("");
374
+ }
375
+ /** For Class attributes.
376
+ * Ex: attribute1: Optional[TypeA] = None */
377
+ _transpileMember(memberName, memberNode) {
378
+ const pythonType = this.getAttributeType(memberNode);
379
+ const isOptional = memberNode.isOptional || memberNode.isNullable;
380
+ let typeAnnotation;
381
+ if (isOptional) {
382
+ this.imports.add(this.lib.optionalType);
383
+ typeAnnotation = pythonType.startsWith("Optional[")
384
+ ? pythonType
385
+ : `Optional[${pythonType}]`;
386
+ }
387
+ else {
388
+ typeAnnotation = pythonType;
389
+ }
390
+ if (memberNode.description && !memberNode.name && !this.isTranspilerable(memberNode)) {
391
+ this.addComment(memberNode.description, `\n${this.indent[1]}`);
392
+ }
393
+ const defaultValue = isOptional ? " = None" : "";
394
+ this.push1(`${memberName}: ${typeAnnotation}${defaultValue}`);
395
+ }
396
+ }
397
+ exports.Zod2Py = Zod2Py;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zod-to-x",
3
- "version": "2.1.0-dev.2",
3
+ "version": "2.2.0",
4
4
  "description": "Multi language types generation from Zod schemas.",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -13,6 +13,8 @@
13
13
  "format:check": "prettier --check .",
14
14
  "test": "find ./test -name \"err-*\" -delete && vitest --run",
15
15
  "test:cpp": "bash ./test/test_zod2cpp/test_cpp.sh",
16
+ "test:py": "bash ./test/test_zod2py/test_py.sh",
17
+ "test:all": "npm run test:cpp && npm run test:py",
16
18
  "ts-run": "ts-node -r tsconfig-paths/register",
17
19
  "prepare": "husky"
18
20
  },