Gathering detailed insights and metrics for zod-to-x
Gathering detailed insights and metrics for zod-to-x
Gathering detailed insights and metrics for zod-to-x
Gathering detailed insights and metrics for zod-to-x
zod-openapi
Convert Zod Schemas to OpenAPI v3.x documentation
@omer-x/json-schema-to-zod
Zod schema generator from JSON schema specs
lynx-form-x
LynxFormX is a lightweight and intuitive form library built for the Lynx framework for mobile development. It streamlines form management by automatically binding fields, handling validation, and providing easy-to-use hooks for custom field manipulation—a
@esmkit/zod-openapi
Convert Zod Schemas to OpenAPI v3.x documentation
npm install zod-to-x
Typescript
Module System
Node Version
NPM Version
TypeScript (58.54%)
C++ (41.33%)
Shell (0.12%)
Total Downloads
0
Last Day
0
Last Week
0
Last Month
0
Last Year
0
MIT License
2 Stars
51 Commits
1 Watchers
4 Branches
1 Contributors
Updated on Jun 03, 2025
Latest Version
2.0.0
Package Id
zod-to-x@2.0.0
Unpacked Size
228.76 kB
Size
48.26 kB
File Count
59
NPM Version
10.8.2
Node Version
20.19.1
Published on
Jun 03, 2025
Cumulative downloads
Total Downloads
Last Day
0%
NaN
Compared to previous day
Last Week
0%
NaN
Compared to previous week
Last Month
0%
NaN
Compared to previous month
Last Year
0%
NaN
Compared to previous year
1
1
Image generated using Canvas AI.
@zod-to-x
is a Zod-based library designed to establish a centralized methodology for defining data structures. It allows you to transpile these definitions into various programming languages in a clear and straightforward way. This tool addresses a fundamental requirement of any software: having a clear understanding of the data it handles.
Important Announcement: 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. 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.
@zod-to-x
@zod-to-x
?Managing data consistency across multiple layers and languages is a common challenge in modern software development. @zod-to-x
solves this by allowing you to define your data models once and effortlessly transpile them into multiple programming languages. Here’s why it stands out:
Centralized Data Definition
Define your data structures in one place using the powerful @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
ecosystem.
Multi-Language Compatibility
Generate data models for TypeScript, Protobuf V3 and C++ (with languages like Golang on the roadmap). No more manually rewriting models for different platforms.
Enhanced Productivity
Automate the transpilation of data models to save time, reduce errors, and let your team focus on business logic instead of boilerplate code.
@zod-to-x
and @zod(*)
dependency.1npm install zod-to-x zod
(*) zod@3.25.0
version or greather is required.
extendZod
method after the first @zod
import:1import { z } from "zod/v4"; 2import { extendZod } from "zod-to-x"; 3extendZod(z);
This extension appends a zod2x
method to:
1import { z } from "zod/v4"; 2import { extendZod, Zod2Ast, Zod2XTranspilers } from "zod-to-x"; 3extendZod(z); // The extend step can be skipped if it has already been done. 4 5/** 6 * 1) Define a data model using Zod schemas 7 */ 8const VisitorSchema = z.object({ 9 id: z.uuid(), 10 name: z.string(), 11 email: z.email(), 12 visitDate: z.date(), 13 comments: z.string().optional(), 14}).zod2x("Visitor"); // Add your expected output type name using the 'zod2x' method. 15 16/** 17 * 2) Build an AST node of the Zod schema 18 */ 19const visitorNodes = new Zod2Ast().build(VisitorSchema); 20 21/** 22 * 3) Generate types in the desired language. 23 * Depending on the transpiled language, data models can be generated using classes. 24 */ 25const tsVisitorAsInterface: string = new Zod2XTranspilers.Zod2Ts().transpile(visitorNodes); 26console.log(tsVisitorAsInterface) 27// output: 28// export interface Visitor { 29// id: string; 30// name: string; 31// email: string; 32// visitDate: Date; 33// comments?: string; 34// } 35 36const tsVisitorAsClass: string = new Zod2XTranspilers.Zod2Ts({outType: "class"}).transpile(visitorNodes); 37console.log(tsVisitorAsClass) 38// output: 39// export class Visitor { 40// id: string; 41// name: string; 42// email: string; 43// visitDate: Date; 44// comments?: string; 45 46// constructor(data: Visitor) { 47// this.id = data.id; 48// this.name = data.name; 49// this.email = data.email; 50// this.visitDate = data.visitdate; 51// this.comments = data.comments; 52// } 53// }
Starting from v1.3.0
, a best practices helper is enabled by default when handling data intersections and unions:
These rules help ensure that each data model follows the single responsibility principle. Otherwise, an error will be thrown when building an ASTNode
.
NOTE: You can disable these rules at your own risk by setting the strict
flag to false
when building the ASTNode
data model. However, any unexpected behavior or issues resulting from this will not be supported.
1const DataA = z.object({ keyA: z.string(), keyC: z.string() }).zod2x("DataA"); 2const DataB = z.object({ keyB: z.string(), keyC: z.number() }).zod2x("DataB"); 3const Intersection = z.intersection(DataA, DataB).zod2x("Intersection"); 4 5// C++ case (with inheritance) 6// struct Intersection : public DataA, public DataB { 7// // Intersection fields are inherited from base structs. 8// }; 9 10// Typescript case (with attribute merging) 11// class Intersection { 12// keyA: string; 13// keyB: string; 14// keyC: number; // Shared key overriden using the last type. 15// };
1const DataA = z.object({ keyA: z.string(), keyD: z.string() }).zod2x("DataA"); 2const DataB = z.object({ keyB: z.string(), keyD: z.string() }).zod2x("DataB"); 3const Intersection = z.intersection(DataA, DataB).zod2x("Intersection"); 4 5// C++ case (with variant type) 6// using Intersection = std::variant<DataA, DataB>; 7 8// Typescript case (with attributes merge) 9// class Intersection { 10// keyA?: string; // Different keys are set as optional 11// keyB?: string; 12// keyD: number; // Shared key preserved as is. 13// };
To enhance data modeling and the serialization/deserialization of a discriminated union type, two key steps should be considered:
ZodEnum
the define the discriminator key options.ZodLiteral
to define the specific value of the discriminator key.Example of discriminant definition:
1// Define message types 2export const MsgType = z.enum(["TypeA", "TypeB"]).zod2x("MsgType"); 3 4// Define different message structures based on the 'msgType' value. 5const MessageA = z 6 .object({ 7 key: z.string(), 8 9 // Assign the type using the corresponding MsgType value and link it with 'zod2x'. 10 msgType: z.literal(MsgType.Values.TypeA).zod2x(MsgType), 11 }) 12 .zod2x("MessageA"); 13 14const MessageB = z 15 .object({ 16 otherKey: z.string(), 17 18 // Same process as MessageA. 19 msgType: z.literal(MsgType.Values.TypeB).zod2x(MsgType), 20 }) 21 .zod2x("MessageB"); 22 23// Define the main Message type using ZodDiscriminatedUnion. 24export const Message = z 25 .discriminatedUnion("msgType", [MessageA, MessageB]) 26 .zod2x("Message");
Example of C++ output:
1// Discriminated union with direct serialization/deserialization 2inline void from_json(const json& j, Message& x) { 3 const auto& k = j.at("msgType").get<std::string>(); 4 if (k == "TypeA") { 5 x = j.get<MessageA>(); 6 } 7 else if (k == "TypeB") { 8 x = j.get<MessageB>(); 9 } 10 else { 11 throw std::runtime_error("Failed to deserialize Message: unknown format"); 12 } 13} 14 15 16 17// Using a regular union or a discriminated union without the above approach: 18// Fallback to try-catch for serialization/deserialization 19inline void from_json(const json& j, Message& x) { 20 try { 21 x = j.get<MessageA>(); 22 return; 23 } catch (const std::exception&) { 24 } 25 try { 26 x = j.get<MessageB>(); 27 return; 28 } catch (const std::exception&) { 29 throw std::runtime_error("Failed to deserialize Message: unknown format"); 30 } 31}
To improve Separation of Concerns (SoC), the Dependency Rule, and Maintainability, a new layer-based modeling approach was introduced in v1.4.0
. This approach establishes relationships between models in separate files, which are transpiled into file imports. This feature provides a powerful tool not only for type systems with robust architectures, such as Clean, Hexagonal, or Layered, but also enforces their usage.
To achieve this, new components are included:
transpile()
method that receives the target language class.true
.v1.4.6
, primitive data types and arrays are also transpiled when using Layered modeling if they are declared as property inside the layer class. They are output as aliases of the types they represent. Setting it to false
will disable this behavior (except for arrays, which are always transpiled). Default is true
.v2.0.0
, a global interface was transpiled with all layer properties. With externalInheritance
and basicType
, this behavior has started to become obsolete, so it is not included by default. If needed, set to false
.1@Domain({ namespace: "USER", file: "user.entity" }) 2class UserModels extends Zod2XModel { 3 4 userRole = z.enum(["Admin", "User"]).zod2x("UserRole"); // (*) 5 6 userEmail = z.email(); // This will be transpiled as an alias. 7 8 userEntity = z.object({ 9 id: z.uuid(), 10 name: z.string().min(1), 11 email: this.userEmail, 12 age: z.int().nonnegative().optional(), 13 role: this.userRole, 14 }); // (*) 15} 16 17const userModels = new UserModels(); 18console.log(userModels.transpile(Zod2XTranspilers.Zod2Ts)); 19// Output: 20// export enum UserRole { 21// Admin = "Admin", 22// User = "User", 23// } 24 25// export type UserEmail = string; 26 27// export interface UserEntity { 28// id: string; 29// name: string; 30// email: UserEmail; 31// age?: number; 32// role: UserRole; 33// } 34
(*) Thanks to the Layer decorator, the use of zod2x
for type naming can now be omitted, simplifying and clarifying the process of model definition. By default, it will take the property name and apply a Pascal case convention (example: myProperty -> MyProperty).
You can still use it if you want to enforce a different type name.
1// Create DTOs using previously defined user models. 2@Application({ namespace: "USER_DTOS", file: "user.dtos" }) 3class UserDtos extends Zod2XModel { 4 5 createUserUseCaseDto = userModels.userEntity.omit({ id: true }); // (*) 6 7 createUserUseCaseResultDto = userModels.userEntity 8 .omit({ role: true }) 9 .extend({ 10 createdAt: z.date(), 11 updatedAt: z.date(), 12 }); // (*) 13} 14 15const userDtos = new UserDtos(); 16console.log(userDtos.transpile(Zod2XTranspilers.Zod2Ts)) 17// Output: 18// import * as USER from "./user.entity"; <--- Reusing models from other layers. 19 20// export interface CreateUserUseCaseDto { 21// name: string; 22// email: USER.UserEmail; 23// age?: number; 24// role: USER.UserRole; 25// } 26 27// export interface CreateUserUseCaseResultDto { 28// id: string; 29// name: string; 30// email: USER.UserEmail; 31// age?: number; 32// createdAt: Date; 33// updatedAt: Date; 34// } 35
(*) Any modification of an existing model (in this case, userEntity
) will lose the relation with that model, but not its children. In the case of the CreateUserUseCaseDto
type, the role
field will remain linked to the existing one in the user.entity
file.
3 - Are your models too large? Simplify them!
If you are dealing with complex models whose definitions are too extensive, split them into multiple classes and then combine them using Zod2XMixin
as shown below:
1import { Zod2XMixin } from "zod-to-x"; 2 3// Sub-models do not require a layer decorator; it is applied automatically when inherited by the main model. 4 5// Sub-model 1 6class CreateUserUseCaseDto { 7 createUserUseCaseDto = userModels.userEntity.omit({ id: true }); 8} 9 10// Sub-model 2 11class CreateUserUseCaseResultDto { 12 createUserUseCaseResultDto = userModels.userEntity 13 .omit({ role: true }) 14 .extend({ 15 createdAt: z.date(), 16 updatedAt: z.date(), 17 }); 18} 19 20// Main model 21@Application({ namespace: "USER_DTOS", file: "user.dtos" }) 22class UserDtos extends Zod2XMixin( 23 [CreateUserUseCaseDto, CreateUserUseCaseResultDto], // Inherit the desired sub-models. 24 Zod2XModel // Zod2XModel must still be the base class. 25) {} 26 27export const userDtos = new UserDtos(); 28console.log(userDtos.transpile(Zod2XTranspilers.Zod2Ts)) 29// Output (same as above): 30// import * as USER from "./user.entity"; <--- Reusing models from other layers. 31 32// export interface CreateUserUseCaseDto { 33// name: string; 34// email: string; 35// age?: number; 36// role: USER.UserRole; 37// } 38 39// export interface CreateUserUseCaseResultDto { 40// id: string; 41// name: string; 42// email: string; 43// age?: number; 44// createdAt: Date; 45// updatedAt: Date; 46// }
4 - Difference of using externalInheritance (defaults) or not.
1// Default output (externalInheritance = true) 2@Application({ namespace: "USER_DTOS", file: "user.dtos" }) 3class UserDtos extends Zod2XModel { 4 5 createUserUseCaseDto = userModels.userEntity.omit({ id: true }); 6 7 createUserUseCaseResultDto = userModels.userEntity; 8} 9 10// Output: 11// import * as USER from "./user.entity"; 12 13// export interface CreateUserUseCaseDto { 14// name: string; 15// email: string; 16// age?: number; 17// role: USER.UserRole; 18// } 19 20// export interface CreateUserUseCaseResultDto extends USER.UserEntity {} 21 22// --------------- 23// If `USER.UserEntity` were a Union or a Discriminated Union, the output would be a Type equivalent to `USER.UserEntity` rather than an Interface that extends it. 24
1// Output without externalInheritance 2@Application({ namespace: "USER_DTOS", file: "user.dtos", externalInheritance: false, skipLayerInterface: false}) 3class UserDtos extends Zod2XModel { 4 5 createUserUseCaseDto = userModels.userEntity.omit({ id: true }); 6 7 createUserUseCaseResultDto = userModels.userEntity; 8} 9 10// Output: 11// import * as USER from "./user.entity"; 12 13// export interface CreateUserUseCaseDto { 14// name: string; 15// email: string; 16// age?: number; 17// role: USER.UserRole; 18// } 19 20// export interface UserDtos { 21// createUserUseCaseDto: CreateUserUseCaseDto; 22// createUserUseCaseResultDto: USER.UserEntity; 23// } 24 25// --------------- 26// In this case, the type of `createUserUseCaseResultDto` is inferred from the parent model (`UserDtos`, only if skipLayerInterface = false), but there is no explicit definition of the type itself.
If the provided layer decorators do not meet your requirements, you can easily define custom ones:
1import { Layer, IZod2xLayerMetadata, Zod2XModel } from "zod-to-x"; 2 3// Create an enumerate with layer indexes 4enum MyLayers { 5 MyDomainLayer = 0, 6 MyApplicationLayer = 1, 7 MyInfrastructureLayer = 2, 8 // ... 9} 10 11// Create custom layer decorators 12function MyDomainLayer(opt: Omit<IZod2xLayerMetadata, "index">) { 13 return Layer({ ...opt, index: MyLayers.MyDomainLayer }); 14} 15// ... 16 17// Use the custom decorators 18@MyDomainLayer({file: "...", namespace: "..."}) 19class MyEntityModels extends Zod2XModel { 20 // ... 21}
Common options:
true
.true
.interface
.false
.Nlohmann
dependency is used for data serialization/deserialization. For C++11, Boost
dependency is used. For C++17 or newer, standard libraries are used.
null
. Defaults to false
.struct
.false
.false
.Additional useful tools to convert Zod Schemas into different formats.
zod2ProtoV3
In case of use of Google protobuf to improve communication performance, you can automatically generate proto
files directly from your models. Check out this input example and its output
Options:
false
.Limitations:
1import { z } from "zod/v4"; 2import { extendZod, Zod2XConverters } from 'zod-to-x'; 3extendZod(z); 4 5// Example using model from `Quick-start` section 6const visitorProtobuf = Zod2XConverters.zod2ProtoV3(VisitorSchema); 7console.log(visitorProtobuf); // Proto file of Visitor's model 8 9// Example using model from `Layer modeling` section 10const createUserDtoProtobuf = Zod2XConverters.zod2ProtoV3(userDtos.createUserUseCaseDto); 11console.log(createUserDtoProtobuf); // Proto file of CreateUserUseCaseDto's model
For a detailed mapping of supported Zod types across supported targets, please refer to the SUPPORTED_ZOD_TYPES.md file.
Choose the approach that best suits your needs, either Layered or non-Layered modeling, and avoid mixing them whenever possible, especially when working with enumerations, objects, unions, or intersections (except for ZodLiteral.zod2x()
, which simply links a literal value to the enumeration it belongs to, if applicable). Their metadata handling differs slightly, which may result in some types not being transpiled as expected.
In Layered modeling, the transpilation of primitive types can be completely disabled or combined by defining them outside the layer class:
1// External primitive 2const stringUUID = z.uuid(); 3 4@Domain({ namespace: "USER", file: "user.entity" }) 5class UserModels extends Zod2XModel { 6 7 // Internal primitive 8 userEmail = z.email(); // This will be transpiled as an alias. It is a layer property. 9 10 userEntity = z.object({ 11 id: stringUUID, // "stringUUID" will not be transpiled as an alias. It is not a layer property. 12 email: this.userEmail, 13 }); 14} 15 16export const userModels = new UserModels();
ZodLazy
shall be used:1// Consider the previous UserModels 2 3import { z } from "zod/v4"; 4 5@Application({ namespace: "USER_DTOS", file: "user.dtos", externalInheritance: false}) 6class UserDtos extends Zod2XModel { 7 8 // OK - Type declaration. It creates a new type based on userEntity but without ID. 9 createUserUseCaseDto = userModels.userEntity.omit({ id: true }); 10 11 // OK - Redeclaration of external type. Could be useful for typing coherence. 12 // "createUserUseCaseResultDto" becomes an alias of "userModels.userEntity". 13 createUserUseCaseResultDto = userModels.userEntity; 14 15 // OK - Redeclaration of an internal type. Could be useful for typing coherence. 16 // "createUserUseCaseDtoV2" becomes an alias of "createUserUseCaseDto". 17 createUserUseCaseDtoV2 = this.createUserUseCaseDto; 18 19 // NOK - Redeclaration of an alias. It will be an alias of "userModels.userEntity" 20 // because "createUserUseCaseResultDto" is aliased during runtime. 21 createUserUseCaseResultDtoV2 = this.createUserUseCaseResultDto; 22 23 // OK, but avoid it - Redeclaration of an alias. It will wait until 24 // "createUserUseCaseResultDto" is aliased and then will becosa an alias 25 // of "createUserUseCaseResultDto" 26 createUserUseCaseResultDtoV3 = z.lazy(() => this.createUserUseCaseResultDto), 27}
No vulnerabilities found.
No security vulnerabilities found.