@gabrielbryk/json-schema-to-zod 2.10.1 → 2.11.1
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 +38 -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 +200 -37
- 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
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { resolveUri } from "./resolveUri.js";
|
|
2
|
+
import { buildRefRegistry } from "./buildRefRegistry.js";
|
|
3
|
+
const decodePointerSegment = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
4
|
+
const uriBaseFromRef = (resolvedUri) => {
|
|
5
|
+
const hashIdx = resolvedUri.indexOf("#");
|
|
6
|
+
return hashIdx === -1 ? resolvedUri : resolvedUri.slice(0, hashIdx);
|
|
7
|
+
};
|
|
8
|
+
const isLocalBase = (base, rootBase) => {
|
|
9
|
+
if (!rootBase)
|
|
10
|
+
return false;
|
|
11
|
+
return base === rootBase;
|
|
12
|
+
};
|
|
13
|
+
export const resolveRef = (schemaNode, ref, refs) => {
|
|
14
|
+
const base = refs.currentBaseUri ?? refs.rootBaseUri ?? "root:///";
|
|
15
|
+
// Handle dynamicRef lookup via dynamicAnchors stack
|
|
16
|
+
const isDynamic = typeof schemaNode.$dynamicRef === "string";
|
|
17
|
+
if (isDynamic && refs.dynamicAnchors && ref.startsWith("#")) {
|
|
18
|
+
const name = ref.slice(1);
|
|
19
|
+
for (let i = refs.dynamicAnchors.length - 1; i >= 0; i -= 1) {
|
|
20
|
+
const entry = refs.dynamicAnchors[i];
|
|
21
|
+
if (entry.name === name) {
|
|
22
|
+
const key = `${entry.uri}#${name}`;
|
|
23
|
+
const target = refs.refRegistry?.get(key);
|
|
24
|
+
if (target) {
|
|
25
|
+
return { schema: target.schema, path: target.path, baseUri: target.baseUri, pointerKey: key };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Resolve URI against base
|
|
31
|
+
const resolvedUri = resolveUri(base, ref);
|
|
32
|
+
const [uriBase, fragment] = resolvedUri.split("#");
|
|
33
|
+
const key = fragment ? `${uriBase}#${fragment}` : uriBase;
|
|
34
|
+
let regEntry = refs.refRegistry?.get(key);
|
|
35
|
+
if (regEntry) {
|
|
36
|
+
return { schema: regEntry.schema, path: regEntry.path, baseUri: regEntry.baseUri, pointerKey: key };
|
|
37
|
+
}
|
|
38
|
+
// Legacy recursive ref: treat as dynamic to __recursive__
|
|
39
|
+
if (schemaNode.$recursiveRef) {
|
|
40
|
+
const recursiveKey = `${base}#__recursive__`;
|
|
41
|
+
regEntry = refs.refRegistry?.get(recursiveKey);
|
|
42
|
+
if (regEntry) {
|
|
43
|
+
return {
|
|
44
|
+
schema: regEntry.schema,
|
|
45
|
+
path: regEntry.path,
|
|
46
|
+
baseUri: regEntry.baseUri,
|
|
47
|
+
pointerKey: recursiveKey,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// External resolver hook
|
|
52
|
+
const extBase = uriBaseFromRef(resolvedUri);
|
|
53
|
+
if (refs.resolveExternalRef && extBase && !isLocalBase(extBase, refs.rootBaseUri ?? "")) {
|
|
54
|
+
const loaded = refs.resolveExternalRef(extBase);
|
|
55
|
+
if (loaded) {
|
|
56
|
+
// If async resolver is used synchronously here, it will be ignored; keep simple sync for now
|
|
57
|
+
const maybePromise = loaded;
|
|
58
|
+
const schema = typeof maybePromise.then === "function"
|
|
59
|
+
? undefined
|
|
60
|
+
: loaded;
|
|
61
|
+
if (schema) {
|
|
62
|
+
const { registry } = buildRefRegistry(schema, extBase);
|
|
63
|
+
registry.forEach((entry, k) => refs.refRegistry?.set(k, entry));
|
|
64
|
+
regEntry = refs.refRegistry?.get(key);
|
|
65
|
+
if (regEntry) {
|
|
66
|
+
return {
|
|
67
|
+
schema: regEntry.schema,
|
|
68
|
+
path: regEntry.path,
|
|
69
|
+
baseUri: regEntry.baseUri,
|
|
70
|
+
pointerKey: key,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Backward compatibility: JSON Pointer into root
|
|
77
|
+
if (refs.root && ref.startsWith("#/")) {
|
|
78
|
+
const rawSegments = ref
|
|
79
|
+
.slice(2)
|
|
80
|
+
.split("/")
|
|
81
|
+
.filter((segment) => segment.length > 0)
|
|
82
|
+
.map(decodePointerSegment);
|
|
83
|
+
let current = refs.root;
|
|
84
|
+
for (const segment of rawSegments) {
|
|
85
|
+
if (typeof current !== "object" || current === null)
|
|
86
|
+
return undefined;
|
|
87
|
+
current = current[segment];
|
|
88
|
+
}
|
|
89
|
+
return { schema: current, path: rawSegments, baseUri: base, pointerKey: ref };
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
};
|
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builder functions for composing SchemaRepresentation objects.
|
|
3
|
+
* These track both the Zod expression and its TypeScript type simultaneously.
|
|
4
|
+
*/
|
|
5
|
+
// Primitives
|
|
6
|
+
export const zodString = () => ({
|
|
7
|
+
expression: "z.string()",
|
|
8
|
+
type: "z.ZodString",
|
|
9
|
+
});
|
|
10
|
+
export const zodNumber = () => ({
|
|
11
|
+
expression: "z.number()",
|
|
12
|
+
type: "z.ZodNumber",
|
|
13
|
+
});
|
|
14
|
+
export const zodBoolean = () => ({
|
|
15
|
+
expression: "z.boolean()",
|
|
16
|
+
type: "z.ZodBoolean",
|
|
17
|
+
});
|
|
18
|
+
export const zodNull = () => ({
|
|
19
|
+
expression: "z.null()",
|
|
20
|
+
type: "z.ZodNull",
|
|
21
|
+
});
|
|
22
|
+
export const zodUndefined = () => ({
|
|
23
|
+
expression: "z.undefined()",
|
|
24
|
+
type: "z.ZodUndefined",
|
|
25
|
+
});
|
|
26
|
+
export const zodAny = () => ({
|
|
27
|
+
expression: "z.any()",
|
|
28
|
+
type: "z.ZodAny",
|
|
29
|
+
});
|
|
30
|
+
export const zodUnknown = () => ({
|
|
31
|
+
expression: "z.unknown()",
|
|
32
|
+
type: "z.ZodUnknown",
|
|
33
|
+
});
|
|
34
|
+
export const zodNever = () => ({
|
|
35
|
+
expression: "z.never()",
|
|
36
|
+
type: "z.ZodNever",
|
|
37
|
+
});
|
|
38
|
+
export const zodBigInt = () => ({
|
|
39
|
+
expression: "z.bigint()",
|
|
40
|
+
type: "z.ZodBigInt",
|
|
41
|
+
});
|
|
42
|
+
export const zodDate = () => ({
|
|
43
|
+
expression: "z.date()",
|
|
44
|
+
type: "z.ZodDate",
|
|
45
|
+
});
|
|
46
|
+
// Reference to another schema (potentially recursive)
|
|
47
|
+
export const zodRef = (schemaName) => ({
|
|
48
|
+
expression: schemaName,
|
|
49
|
+
type: `typeof ${schemaName}`,
|
|
50
|
+
});
|
|
51
|
+
// Lazy wrapper for recursive references
|
|
52
|
+
export const zodLazy = (schemaName) => ({
|
|
53
|
+
expression: `z.lazy(() => ${schemaName})`,
|
|
54
|
+
type: `z.ZodLazy<typeof ${schemaName}>`,
|
|
55
|
+
});
|
|
56
|
+
// Typed lazy wrapper when we know the inner type
|
|
57
|
+
export const zodLazyTyped = (schemaName, innerType) => ({
|
|
58
|
+
expression: `z.lazy<${innerType}>(() => ${schemaName})`,
|
|
59
|
+
type: `z.ZodLazy<${innerType}>`,
|
|
60
|
+
});
|
|
61
|
+
// Wrappers that transform inner representations
|
|
62
|
+
export const zodArray = (inner) => ({
|
|
63
|
+
expression: `z.array(${inner.expression})`,
|
|
64
|
+
type: `z.ZodArray<${inner.type}>`,
|
|
65
|
+
});
|
|
66
|
+
export const zodOptional = (inner) => ({
|
|
67
|
+
expression: `${inner.expression}.optional()`,
|
|
68
|
+
type: `z.ZodOptional<${inner.type}>`,
|
|
69
|
+
});
|
|
70
|
+
export const zodNullable = (inner) => ({
|
|
71
|
+
expression: `${inner.expression}.nullable()`,
|
|
72
|
+
type: `z.ZodNullable<${inner.type}>`,
|
|
73
|
+
});
|
|
74
|
+
export const zodNullableWrapper = (inner) => ({
|
|
75
|
+
expression: `z.nullable(${inner.expression})`,
|
|
76
|
+
type: `z.ZodNullable<${inner.type}>`,
|
|
77
|
+
});
|
|
78
|
+
export const zodDefault = (inner, defaultValue) => ({
|
|
79
|
+
expression: `${inner.expression}.default(${defaultValue})`,
|
|
80
|
+
type: `z.ZodDefault<${inner.type}>`,
|
|
81
|
+
});
|
|
82
|
+
export const zodReadonly = (inner) => ({
|
|
83
|
+
expression: `${inner.expression}.readonly()`,
|
|
84
|
+
type: `z.ZodReadonly<${inner.type}>`,
|
|
85
|
+
});
|
|
86
|
+
// Describe doesn't change the type
|
|
87
|
+
export const zodDescribe = (inner, description) => ({
|
|
88
|
+
expression: `${inner.expression}.describe(${JSON.stringify(description)})`,
|
|
89
|
+
type: inner.type,
|
|
90
|
+
});
|
|
91
|
+
// Meta doesn't change the type
|
|
92
|
+
export const zodMeta = (inner, meta) => ({
|
|
93
|
+
expression: `${inner.expression}.meta(${meta})`,
|
|
94
|
+
type: inner.type,
|
|
95
|
+
});
|
|
96
|
+
// Literals
|
|
97
|
+
export const zodLiteral = (value) => ({
|
|
98
|
+
expression: `z.literal(${value})`,
|
|
99
|
+
type: `z.ZodLiteral<${value}>`,
|
|
100
|
+
});
|
|
101
|
+
// Enums
|
|
102
|
+
export const zodEnum = (values) => {
|
|
103
|
+
const valuesStr = `[${values.join(", ")}]`;
|
|
104
|
+
return {
|
|
105
|
+
expression: `z.enum(${valuesStr})`,
|
|
106
|
+
type: `z.ZodEnum<${valuesStr}>`,
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
// Union
|
|
110
|
+
export const zodUnion = (options) => {
|
|
111
|
+
const exprs = options.map((o) => o.expression).join(", ");
|
|
112
|
+
const types = options.map((o) => o.type).join(", ");
|
|
113
|
+
return {
|
|
114
|
+
expression: `z.union([${exprs}])`,
|
|
115
|
+
type: `z.ZodUnion<[${types}]>`,
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
// Discriminated union
|
|
119
|
+
export const zodDiscriminatedUnion = (discriminator, options) => {
|
|
120
|
+
const exprs = options.map((o) => o.expression).join(", ");
|
|
121
|
+
const types = options.map((o) => o.type).join(", ");
|
|
122
|
+
return {
|
|
123
|
+
expression: `z.discriminatedUnion(${JSON.stringify(discriminator)}, [${exprs}])`,
|
|
124
|
+
type: `z.ZodDiscriminatedUnion<${JSON.stringify(discriminator)}, [${types}]>`,
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
// Intersection
|
|
128
|
+
export const zodIntersection = (left, right) => ({
|
|
129
|
+
expression: `z.intersection(${left.expression}, ${right.expression})`,
|
|
130
|
+
type: `z.ZodIntersection<${left.type}, ${right.type}>`,
|
|
131
|
+
});
|
|
132
|
+
// And method (for chaining)
|
|
133
|
+
export const zodAnd = (base, other) => ({
|
|
134
|
+
expression: `${base.expression}.and(${other.expression})`,
|
|
135
|
+
type: `z.ZodIntersection<${base.type}, ${other.type}>`,
|
|
136
|
+
});
|
|
137
|
+
// Tuple
|
|
138
|
+
export const zodTuple = (items) => {
|
|
139
|
+
const exprs = items.map((i) => i.expression).join(", ");
|
|
140
|
+
const types = items.map((i) => i.type).join(", ");
|
|
141
|
+
return {
|
|
142
|
+
expression: `z.tuple([${exprs}])`,
|
|
143
|
+
type: `z.ZodTuple<[${types}]>`,
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
// Record
|
|
147
|
+
export const zodRecord = (key, value) => ({
|
|
148
|
+
expression: `z.record(${key.expression}, ${value.expression})`,
|
|
149
|
+
type: `z.ZodRecord<${key.type}, ${value.type}>`,
|
|
150
|
+
});
|
|
151
|
+
// Map
|
|
152
|
+
export const zodMap = (key, value) => ({
|
|
153
|
+
expression: `z.map(${key.expression}, ${value.expression})`,
|
|
154
|
+
type: `z.ZodMap<${key.type}, ${value.type}>`,
|
|
155
|
+
});
|
|
156
|
+
// Set
|
|
157
|
+
export const zodSet = (value) => ({
|
|
158
|
+
expression: `z.set(${value.expression})`,
|
|
159
|
+
type: `z.ZodSet<${value.type}>`,
|
|
160
|
+
});
|
|
161
|
+
// Object - builds from shape entries
|
|
162
|
+
export const zodObject = (shape) => {
|
|
163
|
+
const exprParts = [];
|
|
164
|
+
const typeParts = [];
|
|
165
|
+
for (const { key, rep, isGetter } of shape) {
|
|
166
|
+
const quotedKey = JSON.stringify(key);
|
|
167
|
+
if (isGetter) {
|
|
168
|
+
// Getter syntax with explicit type annotation
|
|
169
|
+
exprParts.push(`get ${quotedKey}(): ${rep.type} { return ${rep.expression} }`);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
exprParts.push(`${quotedKey}: ${rep.expression}`);
|
|
173
|
+
}
|
|
174
|
+
typeParts.push(`${quotedKey}: ${rep.type}`);
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
expression: `z.object({ ${exprParts.join(", ")} })`,
|
|
178
|
+
type: `z.ZodObject<{ ${typeParts.join(", ")} }>`,
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
// Strict object
|
|
182
|
+
export const zodStrictObject = (shape) => {
|
|
183
|
+
const base = zodObject(shape);
|
|
184
|
+
return {
|
|
185
|
+
expression: `${base.expression}.strict()`,
|
|
186
|
+
type: base.type, // strict() doesn't change the type signature
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
// Catchall
|
|
190
|
+
export const zodCatchall = (base, catchallSchema) => ({
|
|
191
|
+
expression: `${base.expression}.catchall(${catchallSchema.expression})`,
|
|
192
|
+
type: base.type, // catchall doesn't change the base type for inference purposes
|
|
193
|
+
});
|
|
194
|
+
// SuperRefine - doesn't change the type
|
|
195
|
+
export const zodSuperRefine = (base, refineFn) => ({
|
|
196
|
+
expression: `${base.expression}.superRefine(${refineFn})`,
|
|
197
|
+
type: base.type,
|
|
198
|
+
});
|
|
199
|
+
// Refine - doesn't change the type
|
|
200
|
+
export const zodRefine = (base, refineFn) => ({
|
|
201
|
+
expression: `${base.expression}.refine(${refineFn})`,
|
|
202
|
+
type: base.type,
|
|
203
|
+
});
|
|
204
|
+
// Transform - Zod v4 uses ZodPipe<Base, ZodTransform<Output, Input>>
|
|
205
|
+
// Since we don't know the output type at codegen time, use ZodTypeAny for simplicity
|
|
206
|
+
export const zodTransform = (base, transformFn) => ({
|
|
207
|
+
expression: `${base.expression}.transform(${transformFn})`,
|
|
208
|
+
type: `z.ZodPipe<${base.type}, z.ZodTypeAny>`,
|
|
209
|
+
});
|
|
210
|
+
// Pipe
|
|
211
|
+
export const zodPipe = (first, second) => ({
|
|
212
|
+
expression: `${first.expression}.pipe(${second.expression})`,
|
|
213
|
+
type: `z.ZodPipeline<${first.type}, ${second.type}>`,
|
|
214
|
+
});
|
|
215
|
+
// Coerce wrappers
|
|
216
|
+
export const zodCoerceString = () => ({
|
|
217
|
+
expression: "z.coerce.string()",
|
|
218
|
+
type: "z.ZodString",
|
|
219
|
+
});
|
|
220
|
+
export const zodCoerceNumber = () => ({
|
|
221
|
+
expression: "z.coerce.number()",
|
|
222
|
+
type: "z.ZodNumber",
|
|
223
|
+
});
|
|
224
|
+
export const zodCoerceBoolean = () => ({
|
|
225
|
+
expression: "z.coerce.boolean()",
|
|
226
|
+
type: "z.ZodBoolean",
|
|
227
|
+
});
|
|
228
|
+
export const zodCoerceDate = () => ({
|
|
229
|
+
expression: "z.coerce.date()",
|
|
230
|
+
type: "z.ZodDate",
|
|
231
|
+
});
|
|
232
|
+
// Generic method chaining - for any method that doesn't change type
|
|
233
|
+
export const zodChain = (base, method) => ({
|
|
234
|
+
expression: `${base.expression}.${method}`,
|
|
235
|
+
type: base.type,
|
|
236
|
+
});
|
|
237
|
+
// Create a raw representation from expression string (for backward compatibility)
|
|
238
|
+
// This infers the type from the expression using pattern matching
|
|
239
|
+
export const fromExpression = (expression) => ({
|
|
240
|
+
expression,
|
|
241
|
+
type: inferTypeFromExpression(expression),
|
|
242
|
+
});
|
|
243
|
+
/**
|
|
244
|
+
* Infers the TypeScript type from a Zod expression string.
|
|
245
|
+
* This is used for backward compatibility during migration.
|
|
246
|
+
*/
|
|
247
|
+
export const inferTypeFromExpression = (expr) => {
|
|
248
|
+
// Handle z.lazy with explicit type (possibly with method chains like .optional())
|
|
249
|
+
const lazyTypedMatch = expr.match(/^z\.lazy<([^>]+)>\(\s*\(\)\s*=>\s*([A-Za-z0-9_.$]+)\s*\)(\.[a-z]+\(\))*$/);
|
|
250
|
+
if (lazyTypedMatch) {
|
|
251
|
+
let type = `z.ZodLazy<${lazyTypedMatch[1]}>`;
|
|
252
|
+
const methods = lazyTypedMatch[3] || "";
|
|
253
|
+
if (methods.includes(".optional()")) {
|
|
254
|
+
type = `z.ZodOptional<${type}>`;
|
|
255
|
+
}
|
|
256
|
+
if (methods.includes(".nullable()")) {
|
|
257
|
+
type = `z.ZodNullable<${type}>`;
|
|
258
|
+
}
|
|
259
|
+
return type;
|
|
260
|
+
}
|
|
261
|
+
// Handle z.lazy without explicit type (possibly with method chains like .optional())
|
|
262
|
+
const lazyMatch = expr.match(/^z\.lazy\(\s*\(\)\s*=>\s*([A-Za-z0-9_.$]+)\s*\)(\.[a-z]+\(\))*$/);
|
|
263
|
+
if (lazyMatch) {
|
|
264
|
+
let type = `z.ZodLazy<typeof ${lazyMatch[1]}>`;
|
|
265
|
+
const methods = lazyMatch[2] || "";
|
|
266
|
+
if (methods.includes(".optional()")) {
|
|
267
|
+
type = `z.ZodOptional<${type}>`;
|
|
268
|
+
}
|
|
269
|
+
if (methods.includes(".nullable()")) {
|
|
270
|
+
type = `z.ZodNullable<${type}>`;
|
|
271
|
+
}
|
|
272
|
+
return type;
|
|
273
|
+
}
|
|
274
|
+
// Handle .and() method chains - this creates an intersection type
|
|
275
|
+
// Need to find the .and( that's not inside nested parentheses
|
|
276
|
+
const andIndex = findTopLevelMethod(expr, ".and(");
|
|
277
|
+
if (andIndex !== -1) {
|
|
278
|
+
const baseExpr = expr.substring(0, andIndex);
|
|
279
|
+
// Extract the argument to .and() - find the matching closing paren
|
|
280
|
+
const argsStart = andIndex + 5; // length of ".and("
|
|
281
|
+
const argsEnd = findMatchingParen(expr, argsStart - 1);
|
|
282
|
+
if (argsEnd !== -1) {
|
|
283
|
+
const andArg = expr.substring(argsStart, argsEnd);
|
|
284
|
+
const remainder = expr.substring(argsEnd + 1);
|
|
285
|
+
const baseType = inferTypeFromExpression(baseExpr);
|
|
286
|
+
const andType = inferTypeFromExpression(andArg);
|
|
287
|
+
let type = `z.ZodIntersection<${baseType}, ${andType}>`;
|
|
288
|
+
// Handle trailing methods
|
|
289
|
+
if (remainder.includes(".optional()")) {
|
|
290
|
+
type = `z.ZodOptional<${type}>`;
|
|
291
|
+
}
|
|
292
|
+
if (remainder.includes(".nullable()")) {
|
|
293
|
+
type = `z.ZodNullable<${type}>`;
|
|
294
|
+
}
|
|
295
|
+
return type;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Handle z.intersection(X, Y)
|
|
299
|
+
if (expr.startsWith("z.intersection(")) {
|
|
300
|
+
const argsStart = 15; // length of "z.intersection("
|
|
301
|
+
const argsEnd = findMatchingParen(expr, argsStart - 1);
|
|
302
|
+
if (argsEnd !== -1) {
|
|
303
|
+
const args = expr.substring(argsStart, argsEnd);
|
|
304
|
+
// Split on comma at top level (not inside parentheses)
|
|
305
|
+
const commaIndex = findTopLevelComma(args);
|
|
306
|
+
if (commaIndex !== -1) {
|
|
307
|
+
const leftExpr = args.substring(0, commaIndex).trim();
|
|
308
|
+
const rightExpr = args.substring(commaIndex + 1).trim();
|
|
309
|
+
const leftType = inferTypeFromExpression(leftExpr);
|
|
310
|
+
const rightType = inferTypeFromExpression(rightExpr);
|
|
311
|
+
return `z.ZodIntersection<${leftType}, ${rightType}>`;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// Handle z.object({...}) - for objects with getters or complex shapes
|
|
316
|
+
if (expr.startsWith("z.object(")) {
|
|
317
|
+
// Find the end of z.object({...})
|
|
318
|
+
const argsStart = 9; // length of "z.object("
|
|
319
|
+
const argsEnd = findMatchingParen(expr, argsStart - 1);
|
|
320
|
+
if (argsEnd !== -1) {
|
|
321
|
+
const remainder = expr.substring(argsEnd + 1);
|
|
322
|
+
// Base type for any z.object
|
|
323
|
+
let type = "z.ZodObject<Record<string, z.ZodTypeAny>>";
|
|
324
|
+
// Handle method chains after z.object({...})
|
|
325
|
+
if (remainder.includes(".strict()")) {
|
|
326
|
+
// .strict() doesn't change the type
|
|
327
|
+
}
|
|
328
|
+
if (remainder.includes(".optional()")) {
|
|
329
|
+
type = `z.ZodOptional<${type}>`;
|
|
330
|
+
}
|
|
331
|
+
if (remainder.includes(".nullable()")) {
|
|
332
|
+
type = `z.ZodNullable<${type}>`;
|
|
333
|
+
}
|
|
334
|
+
return type;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Handle z.record(K, V)
|
|
338
|
+
if (expr.startsWith("z.record(")) {
|
|
339
|
+
const argsStart = 9; // length of "z.record("
|
|
340
|
+
const argsEnd = findMatchingParen(expr, argsStart - 1);
|
|
341
|
+
if (argsEnd !== -1) {
|
|
342
|
+
const args = expr.substring(argsStart, argsEnd);
|
|
343
|
+
const commaIndex = findTopLevelComma(args);
|
|
344
|
+
if (commaIndex !== -1) {
|
|
345
|
+
const keyExpr = args.substring(0, commaIndex).trim();
|
|
346
|
+
const valueExpr = args.substring(commaIndex + 1).trim();
|
|
347
|
+
const keyType = inferTypeFromExpression(keyExpr);
|
|
348
|
+
const valueType = inferTypeFromExpression(valueExpr);
|
|
349
|
+
return `z.ZodRecord<${keyType}, ${valueType}>`;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// Primitives - MUST come before refMatch which would incorrectly match z.string() as "typeof z"
|
|
354
|
+
if (expr === "z.string()" || expr.startsWith("z.string()."))
|
|
355
|
+
return "z.ZodString";
|
|
356
|
+
if (expr === "z.number()" || expr.startsWith("z.number()."))
|
|
357
|
+
return "z.ZodNumber";
|
|
358
|
+
if (expr === "z.boolean()" || expr.startsWith("z.boolean()."))
|
|
359
|
+
return "z.ZodBoolean";
|
|
360
|
+
if (expr === "z.null()")
|
|
361
|
+
return "z.ZodNull";
|
|
362
|
+
if (expr === "z.undefined()")
|
|
363
|
+
return "z.ZodUndefined";
|
|
364
|
+
if (expr === "z.any()")
|
|
365
|
+
return "z.ZodAny";
|
|
366
|
+
if (expr === "z.unknown()")
|
|
367
|
+
return "z.ZodUnknown";
|
|
368
|
+
if (expr === "z.never()")
|
|
369
|
+
return "z.ZodNever";
|
|
370
|
+
if (expr.startsWith("z.literal("))
|
|
371
|
+
return "z.ZodLiteral<unknown>";
|
|
372
|
+
if (expr.startsWith("z.enum("))
|
|
373
|
+
return "z.ZodEnum<[string, ...string[]]>";
|
|
374
|
+
// Handle simple schema reference (possibly with .optional())
|
|
375
|
+
const refMatch = expr.match(/^([A-Za-z_$][A-Za-z0-9_$]*)(\.[a-z]+\(\))*$/);
|
|
376
|
+
if (refMatch) {
|
|
377
|
+
const baseName = refMatch[1];
|
|
378
|
+
const methods = refMatch[2] || "";
|
|
379
|
+
let type = `typeof ${baseName}`;
|
|
380
|
+
if (methods.includes(".optional()")) {
|
|
381
|
+
type = `z.ZodOptional<${type}>`;
|
|
382
|
+
}
|
|
383
|
+
if (methods.includes(".nullable()")) {
|
|
384
|
+
type = `z.ZodNullable<${type}>`;
|
|
385
|
+
}
|
|
386
|
+
return type;
|
|
387
|
+
}
|
|
388
|
+
// Handle z.array(X)
|
|
389
|
+
const arrayMatch = expr.match(/^z\.array\((.+)\)(\.[a-z]+\(\))*$/);
|
|
390
|
+
if (arrayMatch) {
|
|
391
|
+
const innerType = inferTypeFromExpression(arrayMatch[1]);
|
|
392
|
+
let type = `z.ZodArray<${innerType}>`;
|
|
393
|
+
const methods = arrayMatch[2] || "";
|
|
394
|
+
if (methods.includes(".optional()")) {
|
|
395
|
+
type = `z.ZodOptional<${type}>`;
|
|
396
|
+
}
|
|
397
|
+
if (methods.includes(".nullable()")) {
|
|
398
|
+
type = `z.ZodNullable<${type}>`;
|
|
399
|
+
}
|
|
400
|
+
return type;
|
|
401
|
+
}
|
|
402
|
+
// Handle z.nullable(X)
|
|
403
|
+
const nullableMatch = expr.match(/^z\.nullable\((.+)\)$/);
|
|
404
|
+
if (nullableMatch) {
|
|
405
|
+
const innerType = inferTypeFromExpression(nullableMatch[1]);
|
|
406
|
+
return `z.ZodNullable<${innerType}>`;
|
|
407
|
+
}
|
|
408
|
+
// Handle z.union([...]) - Zod v4 uses readonly arrays for union options
|
|
409
|
+
// Also handle method chains like .optional(), .nullable()
|
|
410
|
+
if (expr.startsWith("z.union([")) {
|
|
411
|
+
const bracketStart = 8; // position of [
|
|
412
|
+
const bracketEnd = findMatchingParen(expr, bracketStart); // position of ]
|
|
413
|
+
if (bracketEnd !== -1) {
|
|
414
|
+
const arrayContent = expr.substring(bracketStart + 1, bracketEnd); // inside the []
|
|
415
|
+
const memberTypes = parseTopLevelArrayElements(arrayContent);
|
|
416
|
+
const types = memberTypes.map(m => inferTypeFromExpression(m.trim()));
|
|
417
|
+
let baseType = `z.ZodUnion<readonly [${types.join(", ")}]>`;
|
|
418
|
+
const remainder = expr.substring(bracketEnd + 2); // skip ] and )
|
|
419
|
+
if (remainder.includes(".optional()")) {
|
|
420
|
+
baseType = `z.ZodOptional<${baseType}>`;
|
|
421
|
+
}
|
|
422
|
+
if (remainder.includes(".nullable()")) {
|
|
423
|
+
baseType = `z.ZodNullable<${baseType}>`;
|
|
424
|
+
}
|
|
425
|
+
return baseType;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// Handle z.discriminatedUnion(...) - Zod v4 uses readonly arrays
|
|
429
|
+
if (expr.startsWith("z.discriminatedUnion(")) {
|
|
430
|
+
let baseType = "z.ZodDiscriminatedUnion<readonly z.ZodTypeAny[], string>";
|
|
431
|
+
if (expr.endsWith(".optional()")) {
|
|
432
|
+
baseType = `z.ZodOptional<${baseType}>`;
|
|
433
|
+
}
|
|
434
|
+
if (expr.endsWith(".nullable()")) {
|
|
435
|
+
baseType = `z.ZodNullable<${baseType}>`;
|
|
436
|
+
}
|
|
437
|
+
return baseType;
|
|
438
|
+
}
|
|
439
|
+
// Fallback
|
|
440
|
+
return "z.ZodTypeAny";
|
|
441
|
+
};
|
|
442
|
+
/**
|
|
443
|
+
* Find a method call at the top level (not inside nested parentheses)
|
|
444
|
+
*/
|
|
445
|
+
const findTopLevelMethod = (expr, method) => {
|
|
446
|
+
let depth = 0;
|
|
447
|
+
for (let i = 0; i < expr.length - method.length; i++) {
|
|
448
|
+
if (expr[i] === '(' || expr[i] === '[' || expr[i] === '{') {
|
|
449
|
+
depth++;
|
|
450
|
+
}
|
|
451
|
+
else if (expr[i] === ')' || expr[i] === ']' || expr[i] === '}') {
|
|
452
|
+
depth--;
|
|
453
|
+
}
|
|
454
|
+
else if (depth === 0 && expr.substring(i, i + method.length) === method) {
|
|
455
|
+
return i;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return -1;
|
|
459
|
+
};
|
|
460
|
+
/**
|
|
461
|
+
* Find the matching closing parenthesis
|
|
462
|
+
*/
|
|
463
|
+
const findMatchingParen = (expr, openIndex) => {
|
|
464
|
+
let depth = 0;
|
|
465
|
+
for (let i = openIndex; i < expr.length; i++) {
|
|
466
|
+
if (expr[i] === '(' || expr[i] === '[' || expr[i] === '{') {
|
|
467
|
+
depth++;
|
|
468
|
+
}
|
|
469
|
+
else if (expr[i] === ')' || expr[i] === ']' || expr[i] === '}') {
|
|
470
|
+
depth--;
|
|
471
|
+
if (depth === 0) {
|
|
472
|
+
return i;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return -1;
|
|
477
|
+
};
|
|
478
|
+
/**
|
|
479
|
+
* Find a comma at the top level (not inside nested parentheses)
|
|
480
|
+
*/
|
|
481
|
+
const findTopLevelComma = (expr) => {
|
|
482
|
+
let depth = 0;
|
|
483
|
+
for (let i = 0; i < expr.length; i++) {
|
|
484
|
+
if (expr[i] === '(' || expr[i] === '[' || expr[i] === '{') {
|
|
485
|
+
depth++;
|
|
486
|
+
}
|
|
487
|
+
else if (expr[i] === ')' || expr[i] === ']' || expr[i] === '}') {
|
|
488
|
+
depth--;
|
|
489
|
+
}
|
|
490
|
+
else if (depth === 0 && expr[i] === ',') {
|
|
491
|
+
return i;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return -1;
|
|
495
|
+
};
|
|
496
|
+
/**
|
|
497
|
+
* Parse array elements at the top level, respecting nested brackets/parens
|
|
498
|
+
*/
|
|
499
|
+
const parseTopLevelArrayElements = (content) => {
|
|
500
|
+
const elements = [];
|
|
501
|
+
let depth = 0;
|
|
502
|
+
let current = "";
|
|
503
|
+
for (let i = 0; i < content.length; i++) {
|
|
504
|
+
const char = content[i];
|
|
505
|
+
if (char === '(' || char === '[' || char === '{') {
|
|
506
|
+
depth++;
|
|
507
|
+
current += char;
|
|
508
|
+
}
|
|
509
|
+
else if (char === ')' || char === ']' || char === '}') {
|
|
510
|
+
depth--;
|
|
511
|
+
current += char;
|
|
512
|
+
}
|
|
513
|
+
else if (char === ',' && depth === 0) {
|
|
514
|
+
if (current.trim()) {
|
|
515
|
+
elements.push(current.trim());
|
|
516
|
+
}
|
|
517
|
+
current = "";
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
current += char;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (current.trim()) {
|
|
524
|
+
elements.push(current.trim());
|
|
525
|
+
}
|
|
526
|
+
return elements;
|
|
527
|
+
};
|
|
528
|
+
/**
|
|
529
|
+
* Check if an expression contains a reference to a recursive schema.
|
|
530
|
+
*/
|
|
531
|
+
export const containsRecursiveRef = (expr, cycleRefNames) => {
|
|
532
|
+
if (!cycleRefNames || cycleRefNames.size === 0)
|
|
533
|
+
return false;
|
|
534
|
+
for (const refName of cycleRefNames) {
|
|
535
|
+
// Check for direct reference or reference within z.lazy, z.array, etc.
|
|
536
|
+
const pattern = new RegExp(`\\b${refName}\\b`);
|
|
537
|
+
if (pattern.test(expr)) {
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return false;
|
|
542
|
+
};
|
|
543
|
+
/**
|
|
544
|
+
* Determines if a property should use getter syntax based on its representation
|
|
545
|
+
* and the current schema context.
|
|
546
|
+
*/
|
|
547
|
+
export const shouldUseGetter = (rep, currentSchemaName, cycleRefNames, cycleComponentByName) => {
|
|
548
|
+
if (!currentSchemaName)
|
|
549
|
+
return false;
|
|
550
|
+
// Check if the expression directly references the current schema (self-recursion)
|
|
551
|
+
if (rep.expression === currentSchemaName)
|
|
552
|
+
return true;
|
|
553
|
+
// Check if expression contains a reference to a cycle member in the same SCC
|
|
554
|
+
if (!cycleRefNames || cycleRefNames.size === 0)
|
|
555
|
+
return false;
|
|
556
|
+
const currentComponent = cycleComponentByName?.get(currentSchemaName);
|
|
557
|
+
if (currentComponent === undefined)
|
|
558
|
+
return false;
|
|
559
|
+
for (const refName of cycleRefNames) {
|
|
560
|
+
const pattern = new RegExp(`\\b${refName}\\b`);
|
|
561
|
+
if (pattern.test(rep.expression)) {
|
|
562
|
+
const refComponent = cycleComponentByName?.get(refName);
|
|
563
|
+
if (refComponent === currentComponent) {
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return false;
|
|
569
|
+
};
|