Imported from rroumenov/zod-to-x (
AGENTS.md). Install upstream withnpx skills add rroumenov/zod-to-x. Copyright stays with the author.
zod-to-x — Agent Instructions
Overview
npm package that transpiles Zod schema-based data models into typed code for multiple languages (TypeScript, Python/Pydantic, C++11/C++17, Dart/json_serializable) and data formats (Protobuf v3, JSON Schema). Uses a layered modeling architecture inspired by Clean Architecture (DDD).
Quick Reference
npm install # Always first — Zod version depends on branch
npm run build # Clean build: rm dist → tsc → tsc-alias
npm run build:debug # Dev build with sourcemaps (tsconfig.dev.json)
npm test # Delete err-* files → vitest --run
npm run test:cpp # Native C++ compilation tests
npm run test:py # Native Python tests (needs venv activated)
npm run test:all # C++ + Python native tests
npm run test:dart # Native Dart tests (dart + build_runner required)
npm run test:go # Native Go tests (Go toolchain required)
npm run format:check # Prettier check
Critical rule: Always run npm run build before npm test. Tests import from dist/.
Architecture
3-Phase Pipeline
Zod Schema → Zod2Ast.build() → AST Nodes → Zod2X.transpile() → string output
- Zod Schema — User defines data models with Zod +
.zod2x("TypeName")metadata - AST —
Zod2Ast.build()converts schemas into language-agnostic AST nodes - Transpile — Concrete
Zod2X<T>subclass renders AST into target language code
Key directories
| Directory | Purpose |
|---|---|
src/core/ |
AST types, Zod2Ast builder, abstract Zod2X base transpiler |
src/core/ast-types/ |
AST node interfaces: simple (string/number/bool/literal/date/any), complex (object/enum/union/intersection/map/set/tuple/array) |
src/transpilers/ |
Concrete transpilers: typescript/, python/, cpp/, dart/, go/ |
src/converters/ |
Data format converters: protobuf_v3/, json_schema_definitions.ts |
src/layered-modeling/ |
DDD decorators (@Domain, @Application, etc.), Zod2XModel, Zod2XMixin |
src/lib/ |
Zod extension (zod_ext.ts) and helpers (zod_helpers.ts) |
test/common/ |
Shared schemas, test utilities (testOutput, createGenericTestSuite) |
test/test_zod2<lang>/ |
Per-language test suites with expected output files |
test/test_issues/ |
Regression tests for specific bugs |
Transpiler Structure
Every transpiler has at minimum:
options.ts— Language-specific options extendingIZodToXOpt(base:header,indent,includeComments,useImports)runner.ts— Class extendingZod2X<T>implementing ~25 abstract methods
Optional: libs.ts — Import/include definitions (Python, C++ use this).
Abstract Methods to Implement (grouped)
| Group | Methods |
|---|---|
| Lifecycle | runBefore(), runAfter() |
| Imports | addImportFromFile(), getTypeFromExternalNamespace() |
| Generics | getGenericTemplatesTranslation() |
| Layered | addExtendedType() |
| Primitives | getStringType(), getBooleanType(), getNumberType(), getLiteralStringType(), getAnyType(), getDateType() |
| Composites | getTupleType(), getSetType(), getMapType(), getRecordType(), getUnionType(), getIntersectionType(), getArrayType() |
| Transpile | transpileEnum(), transpileStruct(), transpileUnion(), transpileIntersection(), transpileAliasedType() |
Conventions
Naming
- Files:
snake_case.ts - Classes:
PascalCasewith prefixes (ASTfor AST types,Zod2for transpilers) - Properties:
camelCasein code; output follows target language conventions - Tests:
kebab-expected/folders,*.test.tsfiles
Zod Extensions
.zod2x("TypeName")— assigns output type name to a schema. In layered modeling its optional.zod2x(zodEnum)— onZodLiteral, links to parent enum (for discriminated unions)createGenericType("T")— creates a generic type placeholder usingz.promise(z.literal("T"))markeruseGenericType(obj, { slot: concreteType })— instantiates a generic object
Metadata
Metadata lives in zodInstance._zod2x with fields: typeName, parentEnum, layer, aliasOf, parentLayer, genericTypes, isGenericChild.
ZodHelpers
Uses _def.typeName strings (not instanceof) for Bun compatibility.
Branches
main/dev— Zod 4main_v1/dev_v1— Zod 3
Test Patterns
Individual Transpiler Tests
Each test/test_zod2<lang>/ contains:
<lang>_supported_schemas.ts— all supported Zod types wrapped in a single object<lang>_supported_schemas.layered.ts— same types via layered modelingzod2<lang>.test.ts— vitest suite comparing output vs expected strings/filesclass-expected/and/orstruct-expected/(orinterface-expected/) — expected output files
Issue Tests
Each issue in test/test_issues/no_id/N/ contains:
case_N.ts— schema definition reproducing the bugcase_N.test-suite.ts— callscreateGenericTestSuite(), exportsrunCaseNSuitestruct-expected/andclass-expected/— expected output files per language
The facade test_noid_issues.test.ts imports and calls all runCaseNSuite().
Cross-language coverage rule: Every issue test MUST include expected output files for ALL supported transpilers where the bug or its fix applies. Currently: TypeScript (.expected_typescript.ts), Python (.expected_python.py), C++ (.expected_cpp.h), Dart (.expected_dart.dart), Go (.expected_go.go). If a bug is in src/core/ (AST or base transpiler), it likely affects all languages. If a bug is in a specific transpiler, check whether other transpilers have the same problem.
Test Utilities
testOutput(output, expected, errPath?)— trims, compares, writeserr-*on failurecreateGenericTestSuite(name, model, transpiler, basePath)— generates struct + class test pairgetSchemas()— factory returning fresh Zod schemas (avoids metadata pollution)
Skills & Prompts
See .github/skills/ for detailed guidance on:
- Adding a new transpiler
- Adding an issue test
- Debugging transpilation
- Understanding the AST system
- Layered modeling
- Test patterns
See .github/prompts/ for reusable task prompts.
Cross-Transpiler Impact Analysis
When investigating or fixing a bug:
- Determine the scope: Is the fix in
src/core/(affects all languages) orsrc/transpilers/<lang>/(language-specific)? - Core fixes propagate: A change in
ast_node.ts,transpiler.ts, orzod_helpers.tscan affect TypeScript, Python, AND C++ output. Test all. - Transpiler-specific fixes may repeat: If a transpiler has a bug in
transpileStruct()or_transpileMember(), check if the equivalent method in other transpilers has the same flaw. Patterns likecheckExtendedTypeInclusionare implemented independently in each runner. - New language = backfill issue tests: When adding a new transpiler, review ALL existing issue test cases and add expected output files for the new language where the bug scenario applies.
Common Pitfalls
- Forgetting to build —
npm run buildbeforenpm test, always - Metadata pollution — Use
getSchemas()factory in tests; never share schema instances z.lazy()vs plain —z.discriminatedUnionandz.intersectionneed plainZodObject, useuseGenericType(obj, types, true)withskipLazy=true- Cross-layer references — The
@Layerdecorator clones schemas for cross-layer refs, settingaliasOf+parentLayer - Decorator singleton — Each decorated class is cached on
constructor.instance; don't instantiate twice - err- files* — Generated on test failure, gitignored, cleaned by
npm test - Single-language issue tests — Never test only one language. If a bug can manifest in multiple transpilers, cover all of them.
Dart Transpiler Notes
The Dart transpiler (src/transpilers/dart/) emits json_annotation + json_serializable code.
Key Dart-specific behaviors
@JsonSerializable(genericArgumentFactories: true)is emitted for generic classes; theirfromJsontakes extrafromJsonTfunction parameters.@JsonKey(fromJson: fn, toJson: fn)is emitted for class-typed fields to preventjson_serializablefrom following typedef chains to transitively-imported types.instanceTypestring check (notinstanceof) is used to detect ASTObject vs ASTEnum for cross-file references, because cross-file refs areASTDefinitionnodes.- Cross-file generic typedefs (e.g.
AdminUserEntity = GenericUserEntity<AdminUserMetadata>) are detected in thetranspile()pre-scan: top-level nodes withparentFile + description (base type name) + non-empty templatesTranslationpopulate_typedefToExtendedTypesorunAfter()generates 2-arg factory helpers. - Discriminated unions emit an abstract sealed class with a dispatcher function. Typedef aliases of disc unions delegate to the entity-level dispatcher via
_discriminatedUnionDelegateFromJson. - Dart test package lives at
test/test_zod2dart/dart_test_pkg/. Thetest_dart.shscript copies files, runsdart run build_runner build, thendart analyze lib/. - Regenerate expected files with
npx vite-node test_dev/generate_dart_expected.tsafter any runner change.