zod-to-x 2.1.0 → 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`.
@@ -15,5 +15,10 @@ export interface IZod2ProtoV3Opt extends IZodToXOpt {
15
15
  * output will be more compact. Default is false.
16
16
  */
17
17
  encodeDoubleAsInt?: boolean;
18
+ /**
19
+ * ProtoV3 fields are optional by default, but this setting makes the "optional" keyword
20
+ * explicit improving compatibility with ProtoV2. Default is false.
21
+ */
22
+ useExplicitOptional?: boolean;
18
23
  }
19
24
  export declare const defaultOpts: IZod2ProtoV3Opt;
@@ -6,5 +6,6 @@ exports.defaultOpts = {
6
6
  indent: 4,
7
7
  keepKeys: false,
8
8
  encodeDoubleAsInt: false,
9
+ useExplicitOptional: false,
9
10
  useImports: false, // Not required for protobuf files
10
11
  };
@@ -17,4 +17,4 @@ import { IZod2ProtoV3Opt } from "./options";
17
17
  * @returns The Protocol Buffers v3 definition as a string.
18
18
  */
19
19
  export declare function zod2ProtoV3(schema: ZodObject<any> | ZodDiscriminatedUnion | ZodUnion<any>, // TODO: fix any to force only ZodObjects
20
- opt?: Pick<IZod2AstOpt, "strict"> & Pick<IZod2ProtoV3Opt, "packageName" | "keepKeys" | "header" | "indent" | "includeComments" | "encodeDoubleAsInt">): string;
20
+ opt?: Pick<IZod2AstOpt, "strict"> & Pick<IZod2ProtoV3Opt, "packageName" | "keepKeys" | "header" | "indent" | "includeComments" | "encodeDoubleAsInt" | "useExplicitOptional">): string;
@@ -26,11 +26,11 @@ const allowedKeyTypes = [
26
26
  class Zod2ProtoV3 extends core_1.Zod2X {
27
27
  constructor(opt = {}) {
28
28
  super(Object.assign(Object.assign({}, options_1.defaultOpts), opt));
29
+ this.commentKey = "//";
29
30
  this.getUnionType = () => {
30
31
  /** Covered by "transpileUnion" method */
31
32
  return "";
32
33
  };
33
- this.getComment = (data, indent = "") => `${indent}// ${data}`;
34
34
  this.getBooleanType = () => "bool";
35
35
  this.getStringType = () => "string";
36
36
  this.getNumberType = (isInt, range) => {
@@ -171,7 +171,10 @@ class Zod2ProtoV3 extends core_1.Zod2X {
171
171
  // Avoid duplicated descriptions for transpiled items.
172
172
  this.addComment(value.description, `\n${this.indent[1]}`);
173
173
  }
174
- this.push1(`${this.getAttributeType(value)} ${this._adaptField(key)} = ${index + 1};`);
174
+ const fieldType = value.isOptional && this.opt.useExplicitOptional
175
+ ? `optional ${this.getAttributeType(value)}`
176
+ : this.getAttributeType(value);
177
+ this.push1(`${fieldType} ${this._adaptField(key)} = ${index + 1};`);
175
178
  });
176
179
  this.push0("}\n");
177
180
  }
@@ -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,
@@ -35,6 +35,7 @@ export declare abstract class Zod2X<T extends IZodToXOpt> {
35
35
  protected preImports: Set<string>;
36
36
  protected imports: Set<string>;
37
37
  protected postImports: Set<string>;
38
+ protected abstract readonly commentKey: string;
38
39
  protected opt: Partial<T>;
39
40
  protected constructor(opt: Partial<T>);
40
41
  /**
@@ -68,10 +69,6 @@ export declare abstract class Zod2X<T extends IZodToXOpt> {
68
69
  * @param aliasOf
69
70
  */
70
71
  protected abstract addExtendedType(name: string, parentNamespace: string, aliasOf: string): void;
71
- /**
72
- * Returns a comment.
73
- */
74
- protected abstract getComment(data: string, indent?: string): string;
75
72
  /**
76
73
  * Returns the keyword representing a string type in the target language.
77
74
  */
@@ -174,6 +171,10 @@ export declare abstract class Zod2X<T extends IZodToXOpt> {
174
171
  * @param data - The AST node representing the aliased type.
175
172
  */
176
173
  protected abstract transpileAliasedType(data: ASTAliasedTypes): void;
174
+ /**
175
+ * Returns a comment.
176
+ */
177
+ protected getComment: (data: string, indent?: string) => string;
177
178
  /**
178
179
  * Determines if the given type token can be transpiled into the target language.
179
180
  * @param token - The type token to check.
@@ -13,6 +13,15 @@ const string_utils_1 = __importDefault(require("../utils/string_utils"));
13
13
  */
14
14
  class Zod2X {
15
15
  constructor(opt) {
16
+ /**
17
+ * Returns a comment.
18
+ */
19
+ this.getComment = (data, indent = "") => {
20
+ return data
21
+ .split("\n")
22
+ .map((line) => `${indent}${this.commentKey} ${line}`)
23
+ .join("\n");
24
+ };
16
25
  // Push with indentation helpers
17
26
  this.push0 = (data) => this.output.push(`${this.indent[0]}${data}`);
18
27
  this.push1 = (data) => this.output.push(`${this.indent[1]}${data}`);
@@ -208,7 +217,7 @@ class Zod2X {
208
217
  _getHeader() {
209
218
  const header = [];
210
219
  if (this.opt.header) {
211
- header.push(...this.opt.header.split("\n").map((i) => this.getComment(i)));
220
+ header.push(this.getComment(this.opt.header));
212
221
  header.push("");
213
222
  }
214
223
  if (this.preImports.size > 0) {
@@ -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 {};
@@ -4,6 +4,7 @@ import { IZod2CppOpt } from "./options";
4
4
  * @description Transpiler for Zod schemas to C++11 code.
5
5
  */
6
6
  export declare class Zod2Cpp extends Zod2X<IZod2CppOpt> {
7
+ protected readonly commentKey = "//";
7
8
  protected serializers: string[];
8
9
  protected useBoost: boolean;
9
10
  protected lib: {
@@ -31,7 +32,6 @@ export declare class Zod2Cpp extends Zod2X<IZod2CppOpt> {
31
32
  protected getGenericTemplatesTranslation(data: ASTNode): string | undefined;
32
33
  protected checkExtendedTypeInclusion(data: ASTNode, type?: "union" | "alias"): boolean;
33
34
  protected runAfter(): void;
34
- protected getComment: (data: string, indent?: string) => string;
35
35
  protected getDateType: () => string;
36
36
  protected getBooleanType: () => string;
37
37
  protected getStringType: () => string;
@@ -16,11 +16,11 @@ const options_1 = require("./options");
16
16
  class Zod2Cpp extends core_1.Zod2X {
17
17
  constructor(opt = {}) {
18
18
  super(Object.assign(Object.assign({}, options_1.defaultOpts), opt));
19
+ this.commentKey = "//";
19
20
  this.getIntersectionType = () => {
20
21
  /** Covered by "transpileIntersection" method */
21
22
  return "";
22
23
  };
23
- this.getComment = (data, indent = "") => `${indent}// ${data}`;
24
24
  this.getDateType = () => this.getStringType(); // Representing ISO date as a string
25
25
  this.getBooleanType = () => "bool";
26
26
  this.getStringType = () => {
@@ -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;
@@ -1,6 +1,7 @@
1
1
  import { ASTAliasedTypes, ASTEnum, ASTIntersection, ASTNode, ASTObject, ASTUnion, Zod2X } from "../../core";
2
2
  import { IZod2TsOpt } from "./options";
3
3
  export declare class Zod2Ts extends Zod2X<IZod2TsOpt> {
4
+ protected readonly commentKey = "//";
4
5
  constructor(opt?: IZod2TsOpt);
5
6
  protected runAfter(): void;
6
7
  protected runBefore(): void;
@@ -13,7 +14,6 @@ export declare class Zod2Ts extends Zod2X<IZod2TsOpt> {
13
14
  }): void;
14
15
  protected getGenericTemplatesTranslation(data: ASTNode): string | undefined;
15
16
  protected checkExtendedTypeInclusion(data: ASTNode, type?: "alias" | "union" | "d-union"): boolean;
16
- protected getComment: (data: string, indent?: string) => string;
17
17
  protected getAnyType: () => string;
18
18
  protected getBooleanType: () => string;
19
19
  protected getDateType: () => string;
@@ -10,7 +10,7 @@ const options_1 = require("./options");
10
10
  class Zod2Ts extends core_1.Zod2X {
11
11
  constructor(opt = {}) {
12
12
  super(Object.assign(Object.assign({}, options_1.defaultOpts), opt));
13
- this.getComment = (data, indent = "") => `${indent}// ${data}`;
13
+ this.commentKey = "//";
14
14
  this.getAnyType = () => "any";
15
15
  this.getBooleanType = () => "boolean";
16
16
  this.getDateType = () => "Date";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zod-to-x",
3
- "version": "2.1.0",
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
  },