@gabrielbryk/json-schema-to-zod 2.10.1 → 2.11.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/AGENTS.md +44 -0
- package/CHANGELOG.md +28 -0
- package/README.md +6 -33
- package/check-types-lift.sh +23 -0
- package/check-types.sh +20 -0
- package/dist/{esm/cli.js → cli.js} +0 -6
- package/dist/{esm/core → core}/analyzeSchema.js +4 -5
- package/dist/core/emitZod.js +263 -0
- package/dist/{esm/generators → generators}/generateBundle.js +26 -13
- package/dist/{esm/index.js → index.js} +6 -0
- package/dist/jsonSchemaToZod.js +17 -0
- package/dist/parsers/parseAllOf.js +125 -0
- package/dist/parsers/parseAnyOf.js +28 -0
- package/dist/{esm/parsers → parsers}/parseArray.js +27 -11
- package/dist/parsers/parseBoolean.js +4 -0
- package/dist/parsers/parseConst.js +22 -0
- package/dist/parsers/parseEnum.js +35 -0
- package/dist/{esm/parsers → parsers}/parseIfThenElse.js +10 -6
- package/dist/parsers/parseMultipleType.js +10 -0
- package/dist/parsers/parseNot.js +14 -0
- package/dist/parsers/parseNull.js +4 -0
- package/dist/parsers/parseNullable.js +12 -0
- package/dist/{esm/parsers → parsers}/parseNumber.js +4 -1
- package/dist/{esm/parsers → parsers}/parseObject.js +167 -31
- package/dist/parsers/parseOneOf.js +365 -0
- package/dist/{esm/parsers → parsers}/parseSchema.js +55 -117
- package/dist/parsers/parseSimpleDiscriminatedOneOf.js +24 -0
- package/dist/{esm/parsers → parsers}/parseString.js +29 -18
- package/dist/types/Types.d.ts +32 -4
- package/dist/types/core/analyzeSchema.d.ts +3 -2
- package/dist/types/generators/generateBundle.d.ts +0 -2
- package/dist/types/index.d.ts +6 -0
- package/dist/types/parsers/parseAllOf.d.ts +2 -2
- package/dist/types/parsers/parseAnyOf.d.ts +2 -2
- package/dist/types/parsers/parseArray.d.ts +2 -2
- package/dist/types/parsers/parseBoolean.d.ts +2 -1
- package/dist/types/parsers/parseConst.d.ts +2 -2
- package/dist/types/parsers/parseDefault.d.ts +2 -2
- package/dist/types/parsers/parseEnum.d.ts +2 -2
- package/dist/types/parsers/parseIfThenElse.d.ts +2 -2
- package/dist/types/parsers/parseMultipleType.d.ts +2 -2
- package/dist/types/parsers/parseNot.d.ts +2 -2
- package/dist/types/parsers/parseNull.d.ts +2 -1
- package/dist/types/parsers/parseNullable.d.ts +2 -2
- package/dist/types/parsers/parseNumber.d.ts +2 -2
- package/dist/types/parsers/parseObject.d.ts +2 -2
- package/dist/types/parsers/parseOneOf.d.ts +2 -2
- package/dist/types/parsers/parseSchema.d.ts +2 -2
- package/dist/types/parsers/parseSimpleDiscriminatedOneOf.d.ts +2 -2
- package/dist/types/parsers/parseString.d.ts +2 -2
- package/dist/types/utils/anyOrUnknown.d.ts +5 -4
- package/dist/types/utils/esmEmitter.d.ts +29 -0
- package/dist/types/utils/extractInlineObject.d.ts +15 -0
- package/dist/types/utils/liftInlineObjects.d.ts +21 -0
- package/dist/types/utils/namingService.d.ts +21 -0
- package/dist/types/utils/resolveRef.d.ts +7 -0
- package/dist/types/utils/schemaRepresentation.d.ts +71 -0
- package/dist/utils/anyOrUnknown.js +13 -0
- package/dist/{esm/utils → utils}/buildRefRegistry.js +4 -0
- package/dist/utils/esmEmitter.js +87 -0
- package/dist/utils/extractInlineObject.js +119 -0
- package/dist/utils/liftInlineObjects.js +476 -0
- package/dist/utils/namingService.js +58 -0
- package/dist/utils/resolveRef.js +92 -0
- package/dist/utils/schemaRepresentation.js +569 -0
- package/docs/IMPROVEMENT-PLAN.md +243 -0
- package/docs/ZOD-V4-RECURSIVE-TYPE-LIMITATIONS.md +292 -0
- package/docs/proposals/bundle-refactor.md +1 -1
- package/docs/proposals/discriminated-union-with-default.md +248 -0
- package/docs/proposals/inline-object-lifting.md +77 -0
- package/eslint.config.js +4 -2
- package/jest.config.mjs +19 -0
- package/package.json +17 -20
- package/scripts/generateWorkflowSchema.ts +0 -1
- package/dist/cjs/Types.js +0 -2
- package/dist/cjs/cli.js +0 -70
- package/dist/cjs/core/analyzeSchema.js +0 -62
- package/dist/cjs/core/emitZod.js +0 -157
- package/dist/cjs/generators/generateBundle.js +0 -510
- package/dist/cjs/index.js +0 -50
- package/dist/cjs/jsonSchemaToZod.js +0 -10
- package/dist/cjs/package.json +0 -1
- package/dist/cjs/parsers/parseAllOf.js +0 -46
- package/dist/cjs/parsers/parseAnyOf.js +0 -18
- package/dist/cjs/parsers/parseArray.js +0 -90
- package/dist/cjs/parsers/parseBoolean.js +0 -5
- package/dist/cjs/parsers/parseConst.js +0 -7
- package/dist/cjs/parsers/parseDefault.js +0 -8
- package/dist/cjs/parsers/parseEnum.js +0 -21
- package/dist/cjs/parsers/parseIfThenElse.js +0 -35
- package/dist/cjs/parsers/parseMultipleType.js +0 -10
- package/dist/cjs/parsers/parseNot.js +0 -12
- package/dist/cjs/parsers/parseNull.js +0 -5
- package/dist/cjs/parsers/parseNullable.js +0 -12
- package/dist/cjs/parsers/parseNumber.js +0 -116
- package/dist/cjs/parsers/parseObject.js +0 -318
- package/dist/cjs/parsers/parseOneOf.js +0 -53
- package/dist/cjs/parsers/parseSchema.js +0 -419
- package/dist/cjs/parsers/parseSimpleDiscriminatedOneOf.js +0 -21
- package/dist/cjs/parsers/parseString.js +0 -317
- package/dist/cjs/utils/anyOrUnknown.js +0 -14
- package/dist/cjs/utils/buildRefRegistry.js +0 -56
- package/dist/cjs/utils/cliTools.js +0 -108
- package/dist/cjs/utils/cycles.js +0 -113
- package/dist/cjs/utils/half.js +0 -7
- package/dist/cjs/utils/jsdocs.js +0 -20
- package/dist/cjs/utils/omit.js +0 -11
- package/dist/cjs/utils/resolveUri.js +0 -16
- package/dist/cjs/utils/withMessage.js +0 -21
- package/dist/cjs/zodToJsonSchema.js +0 -89
- package/dist/esm/core/emitZod.js +0 -153
- package/dist/esm/jsonSchemaToZod.js +0 -6
- package/dist/esm/package.json +0 -1
- package/dist/esm/parsers/parseAllOf.js +0 -43
- package/dist/esm/parsers/parseAnyOf.js +0 -14
- package/dist/esm/parsers/parseBoolean.js +0 -1
- package/dist/esm/parsers/parseConst.js +0 -3
- package/dist/esm/parsers/parseEnum.js +0 -17
- package/dist/esm/parsers/parseMultipleType.js +0 -6
- package/dist/esm/parsers/parseNot.js +0 -8
- package/dist/esm/parsers/parseNull.js +0 -1
- package/dist/esm/parsers/parseNullable.js +0 -8
- package/dist/esm/parsers/parseOneOf.js +0 -49
- package/dist/esm/parsers/parseSimpleDiscriminatedOneOf.js +0 -17
- package/dist/esm/utils/anyOrUnknown.js +0 -10
- package/jest.config.cjs +0 -4
- package/postcjs.cjs +0 -1
- package/postesm.cjs +0 -1
- /package/dist/{esm/Types.js → Types.js} +0 -0
- /package/dist/{esm/parsers → parsers}/parseDefault.js +0 -0
- /package/dist/{esm/utils → utils}/cliTools.js +0 -0
- /package/dist/{esm/utils → utils}/cycles.js +0 -0
- /package/dist/{esm/utils → utils}/half.js +0 -0
- /package/dist/{esm/utils → utils}/jsdocs.js +0 -0
- /package/dist/{esm/utils → utils}/omit.js +0 -0
- /package/dist/{esm/utils → utils}/resolveUri.js +0 -0
- /package/dist/{esm/utils → utils}/withMessage.js +0 -0
- /package/dist/{esm/zodToJsonSchema.js → zodToJsonSchema.js} +0 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Repository Guidelines
|
|
2
|
+
|
|
3
|
+
## Project Structure & Module Organization
|
|
4
|
+
- `src/`: TypeScript source; CLI entry in `src/cli.ts`, library entry in `src/index.ts`, conversion logic in `src/jsonSchemaToZod.ts`, parsers under `src/parsers/`, and helpers in `src/utils/`.
|
|
5
|
+
- `test/`: TS test suite executed via `tsx`; fixtures live in `test/fixtures/`; snapshots in `test/generateSchemaBundle.snap.test.ts`.
|
|
6
|
+
- `docs/`: Design proposals. Build outputs land in `dist/`; do not edit generated files.
|
|
7
|
+
- `scripts/` and `createIndex.ts`: Codegen utilities used during builds.
|
|
8
|
+
|
|
9
|
+
## Build, Test, and Development Commands
|
|
10
|
+
- Install with `pnpm install` (pinned `pnpm@10.x`).
|
|
11
|
+
- `pnpm gen`: Regenerate index exports.
|
|
12
|
+
- `pnpm test`: Run the TSX-based test suite.
|
|
13
|
+
- `pnpm lint`: Lint `src/` and `test/` with ESLint + @typescript-eslint.
|
|
14
|
+
- `pnpm build`: Generate exports, run tests, clear `dist/`, and emit types + CJS/ESM bundles.
|
|
15
|
+
- `pnpm dev`: Watch tests for rapid iteration.
|
|
16
|
+
- `pnpm smoke:esm`: Quick check that the ESM bundle imports and runs.
|
|
17
|
+
|
|
18
|
+
## Coding Style & Naming Conventions
|
|
19
|
+
- Language: TypeScript with NodeNext/ESM; keep `.js` extensions on internal imports. Package ships ESM-only with flat `dist/`; avoid adding CJS outputs or require-based entrypoints. Build config lives in `tsconfig.build.json`.
|
|
20
|
+
- Indentation: 2 spaces; include semicolons; prefer camelCase for functions/vars and PascalCase for types.
|
|
21
|
+
- Avoid CommonJS `require`; ESLint forbids it. Keep functions small and pure.
|
|
22
|
+
- Use existing utility patterns (e.g., `withMessage`, `buildRefRegistry`) before adding helpers.
|
|
23
|
+
|
|
24
|
+
## Code Quality & Best Practices
|
|
25
|
+
- Type safety first: prefer explicit generics and narrow types over `any`; keep option shapes well-typed and avoid implicit `unknown` casts.
|
|
26
|
+
- Embrace SOLID: single-purpose parsers/utilities, extract shared helpers, and inject collaborators instead of hard-coding globals.
|
|
27
|
+
- Validate boundary cases: cyclical refs, `$ref` resolution, discriminated unions, and recursion depth—add targeted tests when touching those paths.
|
|
28
|
+
- Keep emitted code deterministic; avoid data-dependent randomness or network calls in conversion paths.
|
|
29
|
+
|
|
30
|
+
## Testing Guidelines
|
|
31
|
+
- Tests use a lightweight harness in `test/suite.ts`; add new suites under `test/` mirroring module paths.
|
|
32
|
+
- Prefer fixtures in `test/fixtures/` for schema examples; extend snapshots only when behavior intentionally changes.
|
|
33
|
+
- Cover new parsing paths; add regression cases for edge refs/recursion.
|
|
34
|
+
- Run `pnpm test` (or `pnpm dev`) before opening a PR.
|
|
35
|
+
|
|
36
|
+
## Commit & Pull Request Guidelines
|
|
37
|
+
- Open an issue first; PRs without one are usually not considered (`CONTRIBUTING.md`).
|
|
38
|
+
- Commit messages are short and imperative; gitmoji is welcome but optional. Keep related changes in separate commits.
|
|
39
|
+
- PRs should describe the problem, approach, and risks; link the issue and note test coverage or added fixtures. Screenshots are unnecessary unless CLI UX changes.
|
|
40
|
+
- Keep PR scope tight; avoid editing generated `dist/` files. Mention breaking changes explicitly.
|
|
41
|
+
|
|
42
|
+
## Security & Configuration Tips
|
|
43
|
+
- Avoid adding runtime dependencies that fetch remotely; conversions should stay deterministic and offline.
|
|
44
|
+
- When touching CLI paths, ensure `--module`, `--type`, and `$ref` handling stay backward compatible; add tests for recursion and bundle ordering when modifying ref logic.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @gabrielbryk/json-schema-to-zod
|
|
2
2
|
|
|
3
|
+
## 2.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2656c90: Fix Zod v4 type compatibility and improve discriminated union detection
|
|
8
|
+
|
|
9
|
+
### Zod v4 Type Fixes
|
|
10
|
+
|
|
11
|
+
- Remove `ZodEffects` usage which doesn't exist in Zod v4
|
|
12
|
+
- `.superRefine()` and `.refine()` no longer change the schema type
|
|
13
|
+
- `.transform()` now correctly returns `ZodPipe` type instead of `ZodEffects`
|
|
14
|
+
|
|
15
|
+
### Discriminated Union Detection Improvements
|
|
16
|
+
|
|
17
|
+
- Enhanced `findImplicitDiscriminator` to detect discriminators in `allOf` members
|
|
18
|
+
- Properly resolves `$ref` when checking for discriminator values
|
|
19
|
+
- Correctly collects `required` fields from both parent schema and `allOf` members
|
|
20
|
+
|
|
21
|
+
### Cleaner Output Generation
|
|
22
|
+
|
|
23
|
+
- Simplified `parseAllOf` to avoid generating redundant `z.record().superRefine()` patterns
|
|
24
|
+
- Build proper object types with specific property annotations instead of generic `ZodObject<Record<string, ZodTypeAny>>`
|
|
25
|
+
- Intersection types are now properly tracked and reflected in type annotations
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- 63c6a1c: Enable inline object lifting by default with improved naming and cycle guards
|
|
30
|
+
|
|
3
31
|
## 2.10.1
|
|
4
32
|
|
|
5
33
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -53,8 +53,7 @@ json-refs resolve mySchema.json | json-schema-to-zod | prettier --parser typescr
|
|
|
53
53
|
| `--output` | `-o` | A file path to write to. If not supplied stdout will be used. |
|
|
54
54
|
| `--name` | `-n` | The name of the schema in the output |
|
|
55
55
|
| `--depth` | `-d` | Maximum depth of recursion in schema before falling back to `z.any()`. Defaults to 0. |
|
|
56
|
-
| `--
|
|
57
|
-
| `--type` | `-t` | Export a named type along with the schema. Requires `name` to be set and `module` to be `esm`. |
|
|
56
|
+
| `--type` | `-t` | Export a named type along with the schema. Requires `name` to be set. |
|
|
58
57
|
| `--noImport` | `-ni` | Removes the `import { z } from 'zod';` or equivalent from the output. |
|
|
59
58
|
| `--withJsdocs` | `-wj` | Generate jsdocs off of the description property. |
|
|
60
59
|
|
|
@@ -74,28 +73,16 @@ const myObject = {
|
|
|
74
73
|
},
|
|
75
74
|
};
|
|
76
75
|
|
|
77
|
-
const
|
|
76
|
+
const schemaCode = jsonSchemaToZod(myObject);
|
|
78
77
|
|
|
79
78
|
// `type` can be either a string or - outside of the CLI - a boolean. If its `true`, the name of the type will be the name of the schema with a capitalized first letter.
|
|
80
79
|
const moduleWithType = jsonSchemaToZod(myObject, {
|
|
81
80
|
name: "mySchema",
|
|
82
|
-
module: "esm",
|
|
83
81
|
type: true,
|
|
84
82
|
});
|
|
85
|
-
|
|
86
|
-
const cjs = jsonSchemaToZod(myObject, { module: "cjs", name: "mySchema" });
|
|
87
|
-
|
|
88
83
|
const justTheSchema = jsonSchemaToZod(myObject);
|
|
89
84
|
```
|
|
90
85
|
|
|
91
|
-
##### `module`
|
|
92
|
-
|
|
93
|
-
```typescript
|
|
94
|
-
import { z } from "zod";
|
|
95
|
-
|
|
96
|
-
export default z.object({ hello: z.string().optional() });
|
|
97
|
-
```
|
|
98
|
-
|
|
99
86
|
##### `moduleWithType`
|
|
100
87
|
|
|
101
88
|
```typescript
|
|
@@ -105,20 +92,6 @@ export const mySchema = z.object({ hello: z.string().optional() });
|
|
|
105
92
|
export type MySchema = z.infer<typeof mySchema>;
|
|
106
93
|
```
|
|
107
94
|
|
|
108
|
-
##### `cjs`
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
const { z } = require("zod");
|
|
112
|
-
|
|
113
|
-
module.exports = { mySchema: z.object({ hello: z.string().optional() }) };
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
##### `justTheSchema`
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
z.object({ hello: z.string().optional() });
|
|
120
|
-
```
|
|
121
|
-
|
|
122
95
|
#### Example with `$refs` resolved and output formatted
|
|
123
96
|
|
|
124
97
|
```typescript
|
|
@@ -148,10 +121,10 @@ Factored schemas (like object schemas with "oneOf" etc.) is only partially suppo
|
|
|
148
121
|
|
|
149
122
|
The output of this package is not meant to be used at runtime. JSON Schema and Zod does not overlap 100% and the scope of the parsers are purposefully limited in order to help the author avoid a permanent state of chaotic insanity. As this may cause some details of the original schema to be lost in translation, it is instead recommended to use tools such as [Ajv](https://ajv.js.org/) to validate your runtime values directly against the original JSON Schema.
|
|
150
123
|
|
|
151
|
-
That said, it's possible in most cases to
|
|
124
|
+
That said, it's possible in most cases to load the generated ESM with a data URL. Here's an example that you still probably shouldn't use:
|
|
152
125
|
|
|
153
126
|
```typescript
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
127
|
+
const code = jsonSchemaToZod({ type: "string" }, { name: "mySchema" });
|
|
128
|
+
const { mySchema } = await import(`data:text/javascript,${encodeURIComponent(code)}`);
|
|
129
|
+
mySchema.safeParse("Please just use Ajv instead");
|
|
157
130
|
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Generate and typecheck workflow schema with inline object lifting enabled
|
|
3
|
+
|
|
4
|
+
# Create a temporary ESM script
|
|
5
|
+
cat > .tmp-wf-check-lift.ts << 'EOF'
|
|
6
|
+
import yaml from 'js-yaml';
|
|
7
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
8
|
+
import { jsonSchemaToZod } from './src/jsonSchemaToZod.js';
|
|
9
|
+
|
|
10
|
+
const schema = yaml.load(readFileSync('test/fixtures/workflow.yaml', 'utf8'));
|
|
11
|
+
const output = jsonSchemaToZod(schema, {
|
|
12
|
+
name: 'workflowSchemaLifted',
|
|
13
|
+
liftInlineObjects: { enable: true },
|
|
14
|
+
});
|
|
15
|
+
writeFileSync('.tmp-workflow-schema-output-lift.ts', output);
|
|
16
|
+
console.log('Generated .tmp-workflow-schema-output-lift.ts');
|
|
17
|
+
EOF
|
|
18
|
+
|
|
19
|
+
pnpm tsx .tmp-wf-check-lift.ts
|
|
20
|
+
|
|
21
|
+
echo ""
|
|
22
|
+
echo "Type-checking lifted..."
|
|
23
|
+
pnpm tsc --noEmit --module NodeNext --moduleResolution nodenext --target ES2022 --strict --skipLibCheck .tmp-workflow-schema-output-lift.ts
|
package/check-types.sh
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Quick script to generate and typecheck workflow schema
|
|
3
|
+
|
|
4
|
+
# Create a temporary ESM script
|
|
5
|
+
cat > .tmp-wf-check.ts << 'EOF'
|
|
6
|
+
import yaml from 'js-yaml';
|
|
7
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
8
|
+
import { jsonSchemaToZod } from './src/jsonSchemaToZod.js';
|
|
9
|
+
|
|
10
|
+
const schema = yaml.load(readFileSync('test/fixtures/workflow.yaml', 'utf8'));
|
|
11
|
+
const output = jsonSchemaToZod(schema, { name: 'workflowSchema' });
|
|
12
|
+
writeFileSync('.tmp-workflow-schema-output.ts', output);
|
|
13
|
+
console.log('Generated .tmp-workflow-schema-output.ts');
|
|
14
|
+
EOF
|
|
15
|
+
|
|
16
|
+
pnpm tsx .tmp-wf-check.ts
|
|
17
|
+
|
|
18
|
+
echo ""
|
|
19
|
+
echo "Type-checking..."
|
|
20
|
+
pnpm tsc --noEmit --module NodeNext --moduleResolution nodenext --target ES2022 --strict --skipLibCheck .tmp-workflow-schema-output.ts
|
|
@@ -26,11 +26,6 @@ const params = {
|
|
|
26
26
|
value: "number",
|
|
27
27
|
description: "Maximum depth of recursion before falling back to z.any(). Defaults to 0.",
|
|
28
28
|
},
|
|
29
|
-
module: {
|
|
30
|
-
shorthand: "m",
|
|
31
|
-
value: ["esm", "cjs", "none"],
|
|
32
|
-
description: "Module syntax; 'esm', 'cjs' or 'none'. Defaults to 'esm'.",
|
|
33
|
-
},
|
|
34
29
|
type: {
|
|
35
30
|
shorthand: "t",
|
|
36
31
|
value: "string",
|
|
@@ -52,7 +47,6 @@ async function main() {
|
|
|
52
47
|
const zodSchema = jsonSchemaToZod(jsonSchema, {
|
|
53
48
|
name: args.name,
|
|
54
49
|
depth: args.depth,
|
|
55
|
-
module: args.module || "esm",
|
|
56
50
|
noImport: args.noImport,
|
|
57
51
|
type: args.type,
|
|
58
52
|
withJsdocs: args.withJsdocs,
|
|
@@ -2,14 +2,14 @@ import { parseSchema } from "../parsers/parseSchema.js";
|
|
|
2
2
|
import { detectCycles, computeScc } from "../utils/cycles.js";
|
|
3
3
|
import { buildRefRegistry } from "../utils/buildRefRegistry.js";
|
|
4
4
|
export const analyzeSchema = (schema, options = {}) => {
|
|
5
|
-
const {
|
|
6
|
-
if (type &&
|
|
7
|
-
throw new Error("Option `type` requires `name` to be set
|
|
5
|
+
const { name, type, ...rest } = options;
|
|
6
|
+
if (type && !name) {
|
|
7
|
+
throw new Error("Option `type` requires `name` to be set");
|
|
8
8
|
}
|
|
9
9
|
const normalized = {
|
|
10
|
-
module,
|
|
11
10
|
name,
|
|
12
11
|
type,
|
|
12
|
+
module: "esm",
|
|
13
13
|
...rest,
|
|
14
14
|
exportRefs: rest.exportRefs ?? true,
|
|
15
15
|
withMeta: rest.withMeta ?? true,
|
|
@@ -23,7 +23,6 @@ export const analyzeSchema = (schema, options = {}) => {
|
|
|
23
23
|
const dependencies = new Map();
|
|
24
24
|
const { registry: refRegistry, rootBaseUri } = buildRefRegistry(schema);
|
|
25
25
|
const pass1 = {
|
|
26
|
-
module,
|
|
27
26
|
name,
|
|
28
27
|
path: [],
|
|
29
28
|
seen: new Map(),
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { parseSchema } from "../parsers/parseSchema.js";
|
|
2
|
+
import { expandJsdocs } from "../utils/jsdocs.js";
|
|
3
|
+
import { inferTypeFromExpression } from "../utils/schemaRepresentation.js";
|
|
4
|
+
import { EsmEmitter } from "../utils/esmEmitter.js";
|
|
5
|
+
/**
|
|
6
|
+
* Split a z.object({...}).method1().method2() expression into base and method chain.
|
|
7
|
+
* This is needed for Rule 2: Don't chain methods on recursive types.
|
|
8
|
+
*
|
|
9
|
+
* Only splits if the TOP-LEVEL z.object() contains a getter (not nested ones in .and() etc.)
|
|
10
|
+
*/
|
|
11
|
+
const splitObjectMethodChain = (expr) => {
|
|
12
|
+
if (!expr.startsWith("z.object(")) {
|
|
13
|
+
return { base: expr, methodChain: null };
|
|
14
|
+
}
|
|
15
|
+
// Find the matching closing brace for z.object({
|
|
16
|
+
let depth = 1;
|
|
17
|
+
let i = 9; // length of "z.object("
|
|
18
|
+
// Find the opening { of the object literal
|
|
19
|
+
while (i < expr.length && expr[i] !== "{") {
|
|
20
|
+
i++;
|
|
21
|
+
}
|
|
22
|
+
if (i >= expr.length) {
|
|
23
|
+
return { base: expr, methodChain: null };
|
|
24
|
+
}
|
|
25
|
+
const objectLiteralStart = i;
|
|
26
|
+
i++; // move past the {
|
|
27
|
+
// Find the matching }
|
|
28
|
+
while (i < expr.length && depth > 0) {
|
|
29
|
+
const char = expr[i];
|
|
30
|
+
if (char === "{" || char === "(" || char === "[") {
|
|
31
|
+
depth++;
|
|
32
|
+
}
|
|
33
|
+
else if (char === "}" || char === ")" || char === "]") {
|
|
34
|
+
depth--;
|
|
35
|
+
}
|
|
36
|
+
i++;
|
|
37
|
+
}
|
|
38
|
+
const objectLiteralEnd = i - 1; // position of closing }
|
|
39
|
+
// Extract just the top-level object literal content
|
|
40
|
+
const objectLiteralContent = expr.substring(objectLiteralStart, objectLiteralEnd + 1);
|
|
41
|
+
// Check if the TOP-LEVEL object has a getter (not nested ones)
|
|
42
|
+
// A getter in the top-level object would appear as "get " at depth 1
|
|
43
|
+
if (!hasTopLevelGetter(objectLiteralContent)) {
|
|
44
|
+
return { base: expr, methodChain: null };
|
|
45
|
+
}
|
|
46
|
+
// Now find the closing ) for z.object(
|
|
47
|
+
while (i < expr.length && expr[i] !== ")") {
|
|
48
|
+
i++;
|
|
49
|
+
}
|
|
50
|
+
if (i >= expr.length) {
|
|
51
|
+
return { base: expr, methodChain: null };
|
|
52
|
+
}
|
|
53
|
+
i++; // move past the )
|
|
54
|
+
// Everything after is the method chain
|
|
55
|
+
const base = expr.substring(0, i);
|
|
56
|
+
const methodChain = expr.substring(i);
|
|
57
|
+
// Only return a method chain if there actually is one (like .strict() or .meta())
|
|
58
|
+
// Don't split if the method chain is .and() since that's adding more schema, not metadata
|
|
59
|
+
if (!methodChain || methodChain.length === 0 || methodChain.startsWith(".and(")) {
|
|
60
|
+
return { base: expr, methodChain: null };
|
|
61
|
+
}
|
|
62
|
+
return { base, methodChain };
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Check if an object literal has a getter at its top level (not nested).
|
|
66
|
+
*/
|
|
67
|
+
const hasTopLevelGetter = (objectLiteral) => {
|
|
68
|
+
let depth = 0;
|
|
69
|
+
for (let i = 0; i < objectLiteral.length - 4; i++) {
|
|
70
|
+
const char = objectLiteral[i];
|
|
71
|
+
if (char === "{" || char === "(" || char === "[") {
|
|
72
|
+
depth++;
|
|
73
|
+
}
|
|
74
|
+
else if (char === "}" || char === ")" || char === "]") {
|
|
75
|
+
depth--;
|
|
76
|
+
}
|
|
77
|
+
else if (depth === 1 && objectLiteral.substring(i, i + 4) === "get ") {
|
|
78
|
+
// Found "get " at depth 1 (inside the top-level object, not nested)
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
};
|
|
84
|
+
const orderDeclarations = (entries, dependencies) => {
|
|
85
|
+
const repByName = new Map(entries);
|
|
86
|
+
const depGraph = new Map();
|
|
87
|
+
// Seed graph with empty deps for all nodes
|
|
88
|
+
for (const [name] of entries) {
|
|
89
|
+
depGraph.set(name, new Set());
|
|
90
|
+
}
|
|
91
|
+
// Add explicit dependencies (analyzeSchema) filtered to known names
|
|
92
|
+
for (const [from, set] of dependencies.entries()) {
|
|
93
|
+
const onlyKnown = new Set();
|
|
94
|
+
for (const dep of set) {
|
|
95
|
+
if (repByName.has(dep) && dep !== from) {
|
|
96
|
+
onlyKnown.add(dep);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const current = depGraph.get(from) ?? new Set();
|
|
100
|
+
onlyKnown.forEach((d) => current.add(d));
|
|
101
|
+
depGraph.set(from, current);
|
|
102
|
+
}
|
|
103
|
+
// Add regex-detected dependencies from expressions
|
|
104
|
+
const names = Array.from(repByName.keys());
|
|
105
|
+
for (const [name, rep] of entries) {
|
|
106
|
+
const deps = depGraph.get(name) ?? new Set();
|
|
107
|
+
for (const candidate of names) {
|
|
108
|
+
if (candidate === name)
|
|
109
|
+
continue;
|
|
110
|
+
const matcher = new RegExp(`\\b${candidate}\\b`);
|
|
111
|
+
if (matcher.test(rep.expression)) {
|
|
112
|
+
deps.add(candidate);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
depGraph.set(name, deps);
|
|
116
|
+
}
|
|
117
|
+
// Kahn's algorithm with stable ordering
|
|
118
|
+
const indegree = new Map();
|
|
119
|
+
for (const name of names)
|
|
120
|
+
indegree.set(name, 0);
|
|
121
|
+
for (const [name, deps] of depGraph.entries()) {
|
|
122
|
+
const count = deps.size;
|
|
123
|
+
if (count > 0) {
|
|
124
|
+
indegree.set(name, (indegree.get(name) ?? 0) + count);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const queue = names.filter((n) => (indegree.get(n) ?? 0) === 0);
|
|
128
|
+
const ordered = [];
|
|
129
|
+
while (queue.length) {
|
|
130
|
+
const current = queue.shift();
|
|
131
|
+
ordered.push(current);
|
|
132
|
+
for (const dep of depGraph.get(current) ?? []) {
|
|
133
|
+
indegree.set(dep, (indegree.get(dep) ?? 1) - 1);
|
|
134
|
+
if ((indegree.get(dep) ?? 0) === 0) {
|
|
135
|
+
queue.push(dep);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Fallback in case of cycles: append any remaining nodes
|
|
140
|
+
if (ordered.length < names.length) {
|
|
141
|
+
for (const name of names) {
|
|
142
|
+
if (!ordered.includes(name))
|
|
143
|
+
ordered.push(name);
|
|
144
|
+
}
|
|
145
|
+
// const remaining = names.filter((n) => !ordered.includes(n)).sort();
|
|
146
|
+
// ordered.push(...remaining);
|
|
147
|
+
}
|
|
148
|
+
return ordered.map((name) => [name, repByName.get(name)]);
|
|
149
|
+
};
|
|
150
|
+
export const emitZod = (analysis) => {
|
|
151
|
+
const { schema, options, refNameByPointer, cycleRefNames, cycleComponentByName, } = analysis;
|
|
152
|
+
const { name, type, noImport, exportRefs, withMeta, ...rest } = options;
|
|
153
|
+
const declarations = new Map();
|
|
154
|
+
const dependencies = new Map();
|
|
155
|
+
// Fresh name registry for the emission pass.
|
|
156
|
+
// Seed only with reserved ref names (from $ref resolution) and the root name to keep names stable
|
|
157
|
+
// without inheriting inline allocations from the first pass.
|
|
158
|
+
const emitUsedNames = new Set([...refNameByPointer.values()]);
|
|
159
|
+
if (name)
|
|
160
|
+
emitUsedNames.add(name);
|
|
161
|
+
const parsedSchema = parseSchema(schema, {
|
|
162
|
+
name,
|
|
163
|
+
path: [],
|
|
164
|
+
seen: new Map(),
|
|
165
|
+
declarations,
|
|
166
|
+
dependencies,
|
|
167
|
+
inProgress: new Set(),
|
|
168
|
+
refNameByPointer,
|
|
169
|
+
usedNames: emitUsedNames,
|
|
170
|
+
root: schema,
|
|
171
|
+
currentSchemaName: name,
|
|
172
|
+
cycleRefNames,
|
|
173
|
+
cycleComponentByName,
|
|
174
|
+
refRegistry: analysis.refRegistry,
|
|
175
|
+
rootBaseUri: analysis.rootBaseUri,
|
|
176
|
+
...rest,
|
|
177
|
+
withMeta,
|
|
178
|
+
});
|
|
179
|
+
const jsdocs = rest.withJsdocs && typeof schema === "object" && schema !== null && "description" in schema
|
|
180
|
+
? expandJsdocs(typeof schema.description === "string"
|
|
181
|
+
? schema.description
|
|
182
|
+
: "")
|
|
183
|
+
: "";
|
|
184
|
+
const emitter = new EsmEmitter();
|
|
185
|
+
if (!noImport) {
|
|
186
|
+
emitter.addNamedImport("z", "zod");
|
|
187
|
+
}
|
|
188
|
+
if (declarations.size) {
|
|
189
|
+
for (const [refName, rep] of orderDeclarations(Array.from(declarations.entries()), dependencies)) {
|
|
190
|
+
const expression = typeof rep === "string" ? rep : rep.expression;
|
|
191
|
+
if (typeof expression !== "string") {
|
|
192
|
+
throw new Error(`Expected declaration expression for ${refName}`);
|
|
193
|
+
}
|
|
194
|
+
const hintedType = typeof rep === "object" && rep && "type" in rep && typeof rep.type === "string"
|
|
195
|
+
? rep.type
|
|
196
|
+
: undefined;
|
|
197
|
+
const hasLazy = expression.includes("z.lazy(");
|
|
198
|
+
const hasGetter = expression.includes("get ");
|
|
199
|
+
const isUnion = expression.startsWith("z.union(") || expression.startsWith("z.discriminatedUnion(");
|
|
200
|
+
// Check if this union references any cycle members (recursive schemas)
|
|
201
|
+
const referencesRecursiveSchema = isUnion && Array.from(cycleRefNames).some(cycleName => new RegExp(`\\b${cycleName}\\b`).test(expression));
|
|
202
|
+
// Per Zod v4 docs: type annotations should be on GETTERS for recursive types, not on const declarations.
|
|
203
|
+
// TypeScript can infer the type of const declarations.
|
|
204
|
+
// Exceptions that need explicit type annotation:
|
|
205
|
+
// 1. z.lazy() without getters
|
|
206
|
+
// 2. Union types that reference recursive schemas (for proper type inference)
|
|
207
|
+
const needsTypeAnnotation = (hasLazy && !hasGetter) || referencesRecursiveSchema;
|
|
208
|
+
const storedType = needsTypeAnnotation ? (hintedType ?? inferTypeFromExpression(expression)) : undefined;
|
|
209
|
+
// Rule 2 from Zod v4: Don't chain methods on recursive types
|
|
210
|
+
// If the schema has getters (recursive), we need to split it:
|
|
211
|
+
// 1. Emit base schema as _RefName
|
|
212
|
+
// 2. Emit decorated schema as RefName = _RefName.methods()
|
|
213
|
+
if (hasGetter && expression.startsWith("z.object(")) {
|
|
214
|
+
const { base, methodChain } = splitObjectMethodChain(expression);
|
|
215
|
+
if (methodChain) {
|
|
216
|
+
// Emit base schema (internal, not exported)
|
|
217
|
+
// No type annotation needed - type is on getters, TypeScript infers the rest
|
|
218
|
+
const baseName = `_${refName}`;
|
|
219
|
+
emitter.addConst({
|
|
220
|
+
name: baseName,
|
|
221
|
+
expression: base,
|
|
222
|
+
exported: false,
|
|
223
|
+
});
|
|
224
|
+
// Emit decorated schema (exported)
|
|
225
|
+
emitter.addConst({
|
|
226
|
+
name: refName,
|
|
227
|
+
expression: `${baseName}${methodChain}`,
|
|
228
|
+
exported: exportRefs,
|
|
229
|
+
});
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
emitter.addConst({
|
|
234
|
+
name: refName,
|
|
235
|
+
expression,
|
|
236
|
+
exported: exportRefs,
|
|
237
|
+
typeAnnotation: storedType !== "z.ZodTypeAny" ? storedType : undefined,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (name) {
|
|
242
|
+
emitter.addConst({
|
|
243
|
+
name,
|
|
244
|
+
expression: parsedSchema.expression,
|
|
245
|
+
exported: true,
|
|
246
|
+
jsdoc: jsdocs,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
emitter.addDefaultExport({
|
|
251
|
+
expression: parsedSchema.expression,
|
|
252
|
+
jsdoc: jsdocs,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
if (type && name) {
|
|
256
|
+
const typeName = typeof type === "string" ? type : `${name[0].toUpperCase()}${name.substring(1)}`;
|
|
257
|
+
emitter.addTypeExport({
|
|
258
|
+
name: typeName,
|
|
259
|
+
type: `z.infer<typeof ${name}>`,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return emitter.render();
|
|
263
|
+
};
|
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
import { analyzeSchema } from "../core/analyzeSchema.js";
|
|
2
2
|
import { emitZod } from "../core/emitZod.js";
|
|
3
|
+
import { liftInlineObjects } from "../utils/liftInlineObjects.js";
|
|
3
4
|
export const generateSchemaBundle = (schema, options = {}) => {
|
|
4
|
-
const module = options.module ?? "esm";
|
|
5
5
|
if (!schema || typeof schema !== "object") {
|
|
6
6
|
throw new Error("generateSchemaBundle requires an object schema");
|
|
7
7
|
}
|
|
8
|
-
const
|
|
9
|
-
const
|
|
8
|
+
const liftOpts = options.liftInlineObjects ?? {};
|
|
9
|
+
const useLift = liftOpts.enable !== false;
|
|
10
|
+
const liftedSchema = useLift
|
|
11
|
+
? liftInlineObjects(schema, {
|
|
12
|
+
enable: true,
|
|
13
|
+
nameForPath: liftOpts.nameForPath,
|
|
14
|
+
parentName: options.splitDefs?.rootTypeName ?? options.splitDefs?.rootName ?? schema.title,
|
|
15
|
+
dedup: liftOpts.dedup === true,
|
|
16
|
+
allowInDefs: liftOpts.allowInDefs,
|
|
17
|
+
}).schema
|
|
18
|
+
: schema;
|
|
19
|
+
const defs = liftedSchema.$defs || {};
|
|
20
|
+
const definitions = liftedSchema.definitions || {};
|
|
10
21
|
const defNames = Object.keys(defs);
|
|
11
|
-
const { rootName, rootTypeName, defInfoMap
|
|
22
|
+
const { rootName, rootTypeName, defInfoMap } = buildBundleContext(defNames, defs, options);
|
|
12
23
|
const files = [];
|
|
13
|
-
const targets = planBundleTargets(
|
|
24
|
+
const targets = planBundleTargets(liftedSchema, defs, definitions, defNames, options, rootName, defInfoMap, rootTypeName);
|
|
14
25
|
for (const target of targets) {
|
|
15
26
|
const usedRefs = target.usedRefs;
|
|
16
27
|
const zodParts = [];
|
|
17
28
|
for (const member of target.members) {
|
|
18
29
|
const analysis = analyzeSchema(member.schemaWithDefs, {
|
|
19
30
|
...options,
|
|
20
|
-
module,
|
|
21
31
|
name: member.schemaName,
|
|
22
32
|
type: member.typeName,
|
|
23
33
|
parserOverride: createRefHandler(member.defName, defInfoMap, usedRefs, {
|
|
@@ -28,13 +38,13 @@ export const generateSchemaBundle = (schema, options = {}) => {
|
|
|
28
38
|
const zodSchema = emitZod(analysis);
|
|
29
39
|
zodParts.push(zodSchema);
|
|
30
40
|
}
|
|
31
|
-
const finalSchema = buildSchemaFile(zodParts, usedRefs, defInfoMap
|
|
41
|
+
const finalSchema = buildSchemaFile(zodParts, usedRefs, defInfoMap);
|
|
32
42
|
files.push({ fileName: target.fileName, contents: finalSchema });
|
|
33
43
|
}
|
|
34
44
|
// Nested types extraction (optional)
|
|
35
45
|
const nestedTypesEnabled = options.nestedTypes?.enable;
|
|
36
46
|
if (nestedTypesEnabled) {
|
|
37
|
-
const nestedTypes = collectNestedTypes(
|
|
47
|
+
const nestedTypes = collectNestedTypes(liftedSchema, defs, defNames, rootTypeName ?? rootName);
|
|
38
48
|
if (nestedTypes.length > 0) {
|
|
39
49
|
const nestedFileName = options.nestedTypes?.fileName ?? "nested-types.ts";
|
|
40
50
|
const nestedContent = generateNestedTypesFile(nestedTypes);
|
|
@@ -129,7 +139,7 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
129
139
|
// Self-recursion inside object getters can safely reference the schema name
|
|
130
140
|
return refInfo.schemaName;
|
|
131
141
|
}
|
|
132
|
-
return `z.lazy(() => ${refInfo.schemaName})`;
|
|
142
|
+
return `z.lazy<typeof ${refInfo.schemaName}>(() => ${refInfo.schemaName})`;
|
|
133
143
|
}
|
|
134
144
|
return refInfo.schemaName;
|
|
135
145
|
}
|
|
@@ -147,9 +157,7 @@ const createRefHandler = (currentDefName, defInfoMap, usedRefs, allDefs, options
|
|
|
147
157
|
return undefined;
|
|
148
158
|
};
|
|
149
159
|
};
|
|
150
|
-
const buildSchemaFile = (zodCodeParts, usedRefs, defInfoMap
|
|
151
|
-
if (module !== "esm")
|
|
152
|
-
return zodCodeParts.join("\n");
|
|
160
|
+
const buildSchemaFile = (zodCodeParts, usedRefs, defInfoMap) => {
|
|
153
161
|
const groupFileById = new Map();
|
|
154
162
|
for (const info of defInfoMap.values()) {
|
|
155
163
|
if (!groupFileById.has(info.groupId)) {
|
|
@@ -178,9 +186,10 @@ const buildSchemaFile = (zodCodeParts, usedRefs, defInfoMap, module, target) =>
|
|
|
178
186
|
return code.replace(/^import \{ z \} from "zod"\n?/, "");
|
|
179
187
|
})
|
|
180
188
|
.join("\n");
|
|
181
|
-
|
|
189
|
+
const withImports = imports.length
|
|
182
190
|
? body.replace('import { z } from "zod"', `import { z } from "zod"\n${imports.join("\n")}`)
|
|
183
191
|
: body;
|
|
192
|
+
return withImports;
|
|
184
193
|
};
|
|
185
194
|
const planBundleTargets = (rootSchema, defs, definitions, defNames, options, rootName, defInfoMap, rootTypeName) => {
|
|
186
195
|
const targets = [];
|
|
@@ -245,9 +254,13 @@ const planBundleTargets = (rootSchema, defs, definitions, defNames, options, roo
|
|
|
245
254
|
};
|
|
246
255
|
const findRefDependencies = (schema, validDefNames) => {
|
|
247
256
|
const deps = new Set();
|
|
257
|
+
const seen = new WeakSet();
|
|
248
258
|
function traverse(obj) {
|
|
249
259
|
if (obj === null || typeof obj !== "object")
|
|
250
260
|
return;
|
|
261
|
+
if (seen.has(obj))
|
|
262
|
+
return;
|
|
263
|
+
seen.add(obj);
|
|
251
264
|
if (Array.isArray(obj)) {
|
|
252
265
|
obj.forEach(traverse);
|
|
253
266
|
return;
|
|
@@ -24,10 +24,16 @@ export * from "./parsers/parseString.js";
|
|
|
24
24
|
export * from "./utils/anyOrUnknown.js";
|
|
25
25
|
export * from "./utils/buildRefRegistry.js";
|
|
26
26
|
export * from "./utils/cycles.js";
|
|
27
|
+
export * from "./utils/esmEmitter.js";
|
|
28
|
+
export * from "./utils/extractInlineObject.js";
|
|
27
29
|
export * from "./utils/half.js";
|
|
28
30
|
export * from "./utils/jsdocs.js";
|
|
31
|
+
export * from "./utils/liftInlineObjects.js";
|
|
32
|
+
export * from "./utils/namingService.js";
|
|
29
33
|
export * from "./utils/omit.js";
|
|
34
|
+
export * from "./utils/resolveRef.js";
|
|
30
35
|
export * from "./utils/resolveUri.js";
|
|
36
|
+
export * from "./utils/schemaRepresentation.js";
|
|
31
37
|
export * from "./utils/withMessage.js";
|
|
32
38
|
export * from "./zodToJsonSchema.js";
|
|
33
39
|
import { jsonSchemaToZod } from "./jsonSchemaToZod.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { analyzeSchema } from "./core/analyzeSchema.js";
|
|
2
|
+
import { emitZod } from "./core/emitZod.js";
|
|
3
|
+
import { liftInlineObjects } from "./utils/liftInlineObjects.js";
|
|
4
|
+
export const jsonSchemaToZod = (schema, options = {}) => {
|
|
5
|
+
const liftOpts = options.liftInlineObjects ?? {};
|
|
6
|
+
const sourceSchema = liftOpts.enable !== false
|
|
7
|
+
? liftInlineObjects(schema, {
|
|
8
|
+
enable: true,
|
|
9
|
+
nameForPath: liftOpts.nameForPath,
|
|
10
|
+
parentName: options.name,
|
|
11
|
+
dedup: liftOpts.dedup === true,
|
|
12
|
+
allowInDefs: liftOpts.allowInDefs,
|
|
13
|
+
}).schema
|
|
14
|
+
: schema;
|
|
15
|
+
const analysis = analyzeSchema(sourceSchema, options);
|
|
16
|
+
return emitZod(analysis);
|
|
17
|
+
};
|