@gabrielbryk/json-schema-to-zod 2.12.0 → 2.13.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/.github/RELEASE_SETUP.md +120 -0
- package/.github/TOOLING_GUIDE.md +169 -0
- package/.github/dependabot.yml +52 -0
- package/.github/workflows/ci.yml +33 -0
- package/.github/workflows/release.yml +12 -4
- package/.github/workflows/security.yml +40 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.lintstagedrc.json +3 -0
- package/.prettierrc +20 -0
- package/AGENTS.md +7 -0
- package/CHANGELOG.md +13 -4
- package/README.md +9 -9
- package/commitlint.config.js +24 -0
- package/createIndex.ts +4 -4
- package/dist/cli.js +3 -4
- package/dist/core/analyzeSchema.js +28 -5
- package/dist/core/emitZod.js +11 -4
- package/dist/generators/generateBundle.js +67 -92
- package/dist/parsers/parseAllOf.js +11 -12
- package/dist/parsers/parseAnyOf.js +2 -2
- package/dist/parsers/parseArray.js +38 -12
- package/dist/parsers/parseMultipleType.js +2 -2
- package/dist/parsers/parseNumber.js +44 -102
- package/dist/parsers/parseObject.js +138 -393
- package/dist/parsers/parseOneOf.js +57 -100
- package/dist/parsers/parseSchema.js +132 -55
- package/dist/parsers/parseSimpleDiscriminatedOneOf.js +2 -2
- package/dist/parsers/parseString.js +113 -253
- package/dist/types/Types.d.ts +22 -1
- package/dist/types/core/analyzeSchema.d.ts +1 -0
- package/dist/types/generators/generateBundle.d.ts +1 -1
- package/dist/utils/cliTools.js +1 -2
- package/dist/utils/esmEmitter.js +6 -2
- package/dist/utils/extractInlineObject.js +1 -3
- package/dist/utils/jsdocs.js +1 -4
- package/dist/utils/liftInlineObjects.js +76 -15
- package/dist/utils/resolveRef.js +35 -10
- package/dist/utils/schemaRepresentation.js +35 -66
- package/dist/zodToJsonSchema.js +1 -2
- package/docs/IMPROVEMENT-PLAN.md +30 -12
- package/docs/ZOD-V4-RECURSIVE-TYPE-LIMITATIONS.md +70 -25
- package/docs/proposals/allof-required-merging.md +10 -4
- package/docs/proposals/bundle-refactor.md +10 -4
- package/docs/proposals/discriminated-union-with-default.md +18 -14
- package/docs/proposals/inline-object-lifting.md +15 -5
- package/docs/proposals/ref-anchor-support.md +11 -0
- package/output.txt +67 -0
- package/package.json +18 -5
- package/scripts/generateWorkflowSchema.ts +5 -14
- package/scripts/regenerate_bundle.ts +25 -0
- package/tsc_output.txt +542 -0
- package/tsc_output_2.txt +489 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { parseSchema } from "./parseSchema.js";
|
|
2
2
|
import { anyOrUnknown } from "../utils/anyOrUnknown.js";
|
|
3
|
-
import { extractInlineObject } from "../utils/extractInlineObject.js";
|
|
4
3
|
import { resolveRef } from "../utils/resolveRef.js";
|
|
5
4
|
/**
|
|
6
5
|
* Check if a schema is a "required-only" validation constraint.
|
|
@@ -32,9 +31,6 @@ const isRequiredOnlySchema = (schema) => {
|
|
|
32
31
|
/**
|
|
33
32
|
* Generate a superRefine expression that validates required field combinations.
|
|
34
33
|
* This handles the JSON Schema pattern where oneOf is used purely for validation.
|
|
35
|
-
*
|
|
36
|
-
* When isRefinementOnly is true, the expression is just the refinement function body
|
|
37
|
-
* that should be appended with .superRefine() directly to the parent schema.
|
|
38
34
|
*/
|
|
39
35
|
const generateRequiredFieldsRefinement = (requiredCombinations) => {
|
|
40
36
|
const conditions = requiredCombinations.map((fields) => {
|
|
@@ -71,13 +67,13 @@ const collectSchemaProperties = (schema, refs) => {
|
|
|
71
67
|
// Collect from allOf members
|
|
72
68
|
if (Array.isArray(schema.allOf)) {
|
|
73
69
|
for (const member of schema.allOf) {
|
|
74
|
-
if (typeof member !==
|
|
70
|
+
if (typeof member !== "object" || member === null)
|
|
75
71
|
continue;
|
|
76
72
|
let resolvedMember = member;
|
|
77
73
|
// Resolve $ref if needed
|
|
78
74
|
if (resolvedMember.$ref || resolvedMember.$dynamicRef) {
|
|
79
75
|
const resolved = resolveRef(resolvedMember, (resolvedMember.$ref || resolvedMember.$dynamicRef), refs);
|
|
80
|
-
if (resolved && typeof resolved.schema ===
|
|
76
|
+
if (resolved && typeof resolved.schema === "object" && resolved.schema !== null) {
|
|
81
77
|
resolvedMember = resolved.schema;
|
|
82
78
|
}
|
|
83
79
|
else {
|
|
@@ -113,18 +109,18 @@ const setsEqual = (a, b) => {
|
|
|
113
109
|
return true;
|
|
114
110
|
};
|
|
115
111
|
/**
|
|
116
|
-
* Extract
|
|
117
|
-
* Returns
|
|
112
|
+
* Extract discriminator values from a property schema.
|
|
113
|
+
* Returns string values if it's a const or enum, undefined otherwise.
|
|
118
114
|
*/
|
|
119
|
-
const
|
|
120
|
-
if (prop.const !== undefined && typeof prop.const ===
|
|
121
|
-
return prop.const;
|
|
115
|
+
const getDiscriminatorValues = (prop) => {
|
|
116
|
+
if (prop.const !== undefined && typeof prop.const === "string") {
|
|
117
|
+
return [prop.const];
|
|
122
118
|
}
|
|
123
119
|
if (prop.enum &&
|
|
124
120
|
Array.isArray(prop.enum) &&
|
|
125
|
-
prop.enum.length
|
|
126
|
-
|
|
127
|
-
return prop.enum
|
|
121
|
+
prop.enum.length > 0 &&
|
|
122
|
+
prop.enum.every((value) => typeof value === "string")) {
|
|
123
|
+
return prop.enum;
|
|
128
124
|
}
|
|
129
125
|
return undefined;
|
|
130
126
|
};
|
|
@@ -134,22 +130,16 @@ const getConstValue = (prop) => {
|
|
|
134
130
|
*/
|
|
135
131
|
const getNegatedEnumValues = (prop) => {
|
|
136
132
|
if (prop.not &&
|
|
137
|
-
typeof prop.not ===
|
|
133
|
+
typeof prop.not === "object" &&
|
|
138
134
|
prop.not !== null &&
|
|
139
135
|
Array.isArray(prop.not.enum) &&
|
|
140
|
-
prop.not.enum.every((v) => typeof v ===
|
|
136
|
+
prop.not.enum.every((v) => typeof v === "string")) {
|
|
141
137
|
return prop.not.enum;
|
|
142
138
|
}
|
|
143
139
|
return undefined;
|
|
144
140
|
};
|
|
145
141
|
/**
|
|
146
142
|
* Attempts to find a discriminator property common to all options.
|
|
147
|
-
* A discriminator must:
|
|
148
|
-
* 1. Be present in 'properties' of all options (resolving $refs and allOf if needed)
|
|
149
|
-
* 2. Be required in all options (checking both direct required and allOf required)
|
|
150
|
-
* 3. Have a constant string value (const or enum: [val]) in all options, OR
|
|
151
|
-
* have constant values in all but one option which has not:{enum:[those values]}
|
|
152
|
-
* 4. Have unique values across all options (for const values)
|
|
153
143
|
*/
|
|
154
144
|
const findImplicitDiscriminator = (options, refs) => {
|
|
155
145
|
if (options.length < 2)
|
|
@@ -157,13 +147,13 @@ const findImplicitDiscriminator = (options, refs) => {
|
|
|
157
147
|
// Fully resolve schemas and collect their properties (including from allOf)
|
|
158
148
|
const resolvedOptions = [];
|
|
159
149
|
for (const opt of options) {
|
|
160
|
-
if (typeof opt !==
|
|
150
|
+
if (typeof opt !== "object" || opt === null)
|
|
161
151
|
return undefined;
|
|
162
152
|
let schemaObj = opt;
|
|
163
153
|
// Resolve ref if needed
|
|
164
154
|
if (schemaObj.$ref || schemaObj.$dynamicRef) {
|
|
165
155
|
const resolved = resolveRef(schemaObj, (schemaObj.$ref || schemaObj.$dynamicRef), refs);
|
|
166
|
-
if (resolved && typeof resolved.schema ===
|
|
156
|
+
if (resolved && typeof resolved.schema === "object" && resolved.schema !== null) {
|
|
167
157
|
schemaObj = resolved.schema;
|
|
168
158
|
}
|
|
169
159
|
else {
|
|
@@ -171,7 +161,7 @@ const findImplicitDiscriminator = (options, refs) => {
|
|
|
171
161
|
}
|
|
172
162
|
}
|
|
173
163
|
// Must be an object type
|
|
174
|
-
if (schemaObj.type !==
|
|
164
|
+
if (schemaObj.type !== "object") {
|
|
175
165
|
return undefined;
|
|
176
166
|
}
|
|
177
167
|
// Collect all properties including from allOf
|
|
@@ -190,6 +180,7 @@ const findImplicitDiscriminator = (options, refs) => {
|
|
|
190
180
|
let defaultIndex;
|
|
191
181
|
let defaultEnumValues;
|
|
192
182
|
let isValidDiscriminator = true;
|
|
183
|
+
let optionsWithDiscriminator = 0;
|
|
193
184
|
for (let i = 0; i < resolvedOptions.length; i++) {
|
|
194
185
|
const opt = resolvedOptions[i];
|
|
195
186
|
// Must be required
|
|
@@ -204,25 +195,31 @@ const findImplicitDiscriminator = (options, refs) => {
|
|
|
204
195
|
}
|
|
205
196
|
// Resolve property schema ref if needed (e.g. definitions/kind -> const)
|
|
206
197
|
let prop = propBeforeResolve;
|
|
207
|
-
if (typeof prop ===
|
|
198
|
+
if (typeof prop === "object" && prop !== null && (prop.$ref || prop.$dynamicRef)) {
|
|
208
199
|
const resolvedProp = resolveRef(prop, (prop.$ref || prop.$dynamicRef), refs);
|
|
209
200
|
if (resolvedProp) {
|
|
210
201
|
prop = resolvedProp.schema;
|
|
211
202
|
}
|
|
212
203
|
}
|
|
213
|
-
if (typeof prop !==
|
|
204
|
+
if (typeof prop !== "object" || prop === null) {
|
|
214
205
|
isValidDiscriminator = false;
|
|
215
206
|
break;
|
|
216
207
|
}
|
|
217
208
|
// Check for constant value
|
|
218
|
-
const constValue =
|
|
209
|
+
const constValue = getDiscriminatorValues(prop);
|
|
219
210
|
if (constValue !== undefined) {
|
|
220
|
-
|
|
221
|
-
|
|
211
|
+
optionsWithDiscriminator += 1;
|
|
212
|
+
for (const value of constValue) {
|
|
213
|
+
if (constValuesSet.has(value)) {
|
|
214
|
+
isValidDiscriminator = false; // Duplicate value found
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
constValuesSet.add(value);
|
|
218
|
+
constValues.push(value);
|
|
219
|
+
}
|
|
220
|
+
if (!isValidDiscriminator) {
|
|
222
221
|
break;
|
|
223
222
|
}
|
|
224
|
-
constValuesSet.add(constValue);
|
|
225
|
-
constValues.push(constValue);
|
|
226
223
|
continue;
|
|
227
224
|
}
|
|
228
225
|
// Check for negated enum (default case pattern)
|
|
@@ -245,17 +242,17 @@ const findImplicitDiscriminator = (options, refs) => {
|
|
|
245
242
|
continue;
|
|
246
243
|
}
|
|
247
244
|
// Check if all options have const values (full discriminated union)
|
|
248
|
-
if (
|
|
249
|
-
return { type:
|
|
245
|
+
if (optionsWithDiscriminator === resolvedOptions.length) {
|
|
246
|
+
return { type: "full", key };
|
|
250
247
|
}
|
|
251
248
|
// Check if we have a valid default case pattern
|
|
252
249
|
if (defaultIndex !== undefined &&
|
|
253
250
|
defaultEnumValues !== undefined &&
|
|
254
|
-
|
|
251
|
+
optionsWithDiscriminator === resolvedOptions.length - 1) {
|
|
255
252
|
// Verify the negated enum exactly matches the const values
|
|
256
253
|
const enumSet = new Set(defaultEnumValues);
|
|
257
254
|
if (setsEqual(constValuesSet, enumSet)) {
|
|
258
|
-
return { type:
|
|
255
|
+
return { type: "withDefault", key, defaultIndex, constValues };
|
|
259
256
|
}
|
|
260
257
|
}
|
|
261
258
|
}
|
|
@@ -279,87 +276,47 @@ export const parseOneOf = (schema, refs) => {
|
|
|
279
276
|
}
|
|
280
277
|
// Optimize: Check for implicit discriminated union
|
|
281
278
|
const discriminator = findImplicitDiscriminator(schema.oneOf, refs);
|
|
282
|
-
if (discriminator?.type ===
|
|
279
|
+
if (discriminator?.type === "full") {
|
|
283
280
|
// All options have constant discriminator values
|
|
284
281
|
const options = schema.oneOf.map((s, i) => parseSchema(s, {
|
|
285
282
|
...refs,
|
|
286
283
|
path: [...refs.path, "oneOf", i],
|
|
287
284
|
}));
|
|
288
|
-
const expressions = options.map(o => o.expression).join(", ");
|
|
289
|
-
const types = options.map(o => o.type).join(", ");
|
|
285
|
+
const expressions = options.map((o) => o.expression).join(", ");
|
|
286
|
+
const types = options.map((o) => o.type).join(", ");
|
|
290
287
|
return {
|
|
291
288
|
expression: `z.discriminatedUnion("${discriminator.key}", [${expressions}])`,
|
|
292
289
|
// Use readonly tuple for union type annotations (required for recursive type inference)
|
|
293
290
|
type: `z.ZodDiscriminatedUnion<"${discriminator.key}", readonly [${types}]>`,
|
|
294
291
|
};
|
|
295
292
|
}
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
293
|
+
// Fallback: Use z.xor for exclusive unions
|
|
294
|
+
// z.xor takes exactly two arguments.
|
|
295
|
+
// If more than 2, we must nest them: z.xor(A, z.xor(B, C)) ?
|
|
296
|
+
// Or usage says: export function xor<const T extends readonly core.SomeType[]>(options: T, ...): ZodXor<T>
|
|
297
|
+
// Wait, let's check `schemas.ts` again.
|
|
298
|
+
// export function xor<const T extends readonly core.SomeType[]>(options: T, params?: ...): ZodXor<T>
|
|
299
|
+
// It takes an array of options!
|
|
300
|
+
// Wait, in `from-json-schema.ts` (Zod repo), how is it used?
|
|
301
|
+
// It uses `z.xor`.
|
|
302
|
+
// Let's verify `schemas.ts` content I viewed earlier.
|
|
303
|
+
// Line 1368: export function xor<const T extends readonly core.SomeType[]>(options: T, params?: ...): ZodXor<T>
|
|
304
|
+
// Yes, it takes an array `options`.
|
|
305
|
+
// It says "Unlike regular unions that succeed when any option matches, xor fails if zero or more than one option matches the input."
|
|
306
|
+
// Perfect.
|
|
301
307
|
const parsedSchemas = schema.oneOf.map((s, i) => {
|
|
302
|
-
const
|
|
303
|
-
if (extracted) {
|
|
304
|
-
// extractInlineObject returns a refName string
|
|
305
|
-
return { expression: extracted, type: `typeof ${extracted}` };
|
|
306
|
-
}
|
|
307
|
-
let parsed = parseSchema(s, {
|
|
308
|
+
const parsed = parseSchema(s, {
|
|
308
309
|
...refs,
|
|
309
310
|
path: [...refs.path, "oneOf", i],
|
|
310
311
|
});
|
|
311
|
-
// Make regular unions stricter: if it's an object, it shouldn't match emptiness.
|
|
312
|
-
// Ensure we only apply .strict() to actual z.object() calls.
|
|
313
|
-
if (typeof s === "object" &&
|
|
314
|
-
s !== null &&
|
|
315
|
-
(s.type === "object" || s.properties) &&
|
|
316
|
-
!s.$ref &&
|
|
317
|
-
parsed.expression.startsWith("z.object(") && // Critical check: Must be a Zod object
|
|
318
|
-
!parsed.expression.includes(".and(") &&
|
|
319
|
-
!parsed.expression.includes(".intersection(") &&
|
|
320
|
-
!parsed.expression.includes(".strict()") &&
|
|
321
|
-
!parsed.expression.includes(".catchall") &&
|
|
322
|
-
!parsed.expression.includes(".passthrough")) {
|
|
323
|
-
parsed = {
|
|
324
|
-
expression: parsed.expression + ".strict()",
|
|
325
|
-
type: parsed.type, // .strict() doesn't change the Zod type
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
312
|
return parsed;
|
|
329
313
|
});
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
const schemasExpressions = parsedSchemas.map(r => r.expression).join(", ");
|
|
335
|
-
const expression = `${unionExpression}.superRefine((x, ctx) => {
|
|
336
|
-
const schemas = [${schemasExpressions}];
|
|
337
|
-
const errors = schemas.reduce<z.ZodError[]>(
|
|
338
|
-
(errors, schema) =>
|
|
339
|
-
((result) =>
|
|
340
|
-
result.error ? [...errors, result.error] : errors)(
|
|
341
|
-
schema.safeParse(x),
|
|
342
|
-
),
|
|
343
|
-
[],
|
|
344
|
-
);
|
|
345
|
-
if (schemas.length - errors.length !== 1) {
|
|
346
|
-
ctx.addIssue({
|
|
347
|
-
path: [],
|
|
348
|
-
code: "invalid_union",
|
|
349
|
-
errors: errors.map(e => e.issues),
|
|
350
|
-
message: "Invalid input: Should pass single schema",
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
})`;
|
|
354
|
-
return {
|
|
355
|
-
expression,
|
|
356
|
-
// In Zod v4, .superRefine() doesn't change the type
|
|
357
|
-
type: `z.ZodUnion<readonly [${unionTypes}]>`,
|
|
358
|
-
};
|
|
359
|
-
}
|
|
314
|
+
const expressions = parsedSchemas.map((r) => r.expression).join(", ");
|
|
315
|
+
const types = parsedSchemas.map((r) => r.type).join(", ");
|
|
316
|
+
const expression = `z.xor([${expressions}])`;
|
|
317
|
+
const type = `z.ZodXor<readonly [${types}]>`;
|
|
360
318
|
return {
|
|
361
|
-
expression
|
|
362
|
-
|
|
363
|
-
type: `z.ZodUnion<readonly [${unionTypes}]>`,
|
|
319
|
+
expression,
|
|
320
|
+
type,
|
|
364
321
|
};
|
|
365
322
|
};
|
|
@@ -28,9 +28,7 @@ export const parseSchema = (schema, refs = { seen: new Map(), path: [] }, blockM
|
|
|
28
28
|
refs.refNameByPointer = refs.refNameByPointer ?? new Map();
|
|
29
29
|
refs.usedNames = refs.usedNames ?? new Set();
|
|
30
30
|
if (typeof schema !== "object") {
|
|
31
|
-
return schema
|
|
32
|
-
? anyOrUnknown(refs)
|
|
33
|
-
: { expression: "z.never()", type: "z.ZodNever" };
|
|
31
|
+
return schema ? anyOrUnknown(refs) : { expression: "z.never()", type: "z.ZodNever" };
|
|
34
32
|
}
|
|
35
33
|
const parentBase = refs.currentBaseUri ?? refs.rootBaseUri ?? "root:///";
|
|
36
34
|
const baseUri = typeof schema.$id === "string" ? resolveUri(parentBase, schema.$id) : parentBase;
|
|
@@ -43,7 +41,11 @@ export const parseSchema = (schema, refs = { seen: new Map(), path: [] }, blockM
|
|
|
43
41
|
});
|
|
44
42
|
}
|
|
45
43
|
if (refs.parserOverride) {
|
|
46
|
-
const custom = refs.parserOverride(schema, {
|
|
44
|
+
const custom = refs.parserOverride(schema, {
|
|
45
|
+
...refs,
|
|
46
|
+
currentBaseUri: baseUri,
|
|
47
|
+
dynamicAnchors,
|
|
48
|
+
});
|
|
47
49
|
if (typeof custom === "string") {
|
|
48
50
|
// ParserOverride returns string for backward compatibility
|
|
49
51
|
return { expression: custom, type: "z.ZodTypeAny" };
|
|
@@ -71,7 +73,7 @@ export const parseSchema = (schema, refs = { seen: new Map(), path: [] }, blockM
|
|
|
71
73
|
let parsed = selectParser(schema, { ...refs, currentBaseUri: baseUri, dynamicAnchors });
|
|
72
74
|
if (!blockMeta) {
|
|
73
75
|
if (!refs.withoutDescribes) {
|
|
74
|
-
parsed = addDescribes(schema, parsed
|
|
76
|
+
parsed = addDescribes(schema, parsed);
|
|
75
77
|
}
|
|
76
78
|
if (!refs.withoutDefaults) {
|
|
77
79
|
parsed = addDefaults(schema, parsed);
|
|
@@ -123,60 +125,128 @@ const parseRef = (schema, refs) => {
|
|
|
123
125
|
// Check if this is a true forward reference (target not yet declared)
|
|
124
126
|
// We only need z.lazy() for forward refs, not for back-refs to already-declared schemas
|
|
125
127
|
const isForwardRef = refs.inProgress.has(refName);
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
//
|
|
143
|
-
// The
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// Also arrays, unions, and other non-object contexts with forward refs need z.lazy()
|
|
149
|
-
if (isForwardRef || inRecordContext) {
|
|
150
|
-
return {
|
|
151
|
-
expression: `z.lazy(() => ${refName})`,
|
|
152
|
-
type: `z.ZodLazy<${refType}>`
|
|
153
|
-
};
|
|
154
|
-
}
|
|
128
|
+
// Check context: are we inside an object property where getters work?
|
|
129
|
+
// IMPORTANT: additionalProperties becomes z.record() (or .catchall()) which does NOT support getters for deferred evaluation
|
|
130
|
+
// Only named properties (properties, patternProperties) can use getters
|
|
131
|
+
// additionalProperties becomes z.record() value - getters don't work there
|
|
132
|
+
// Per Zod issue #4881: z.record() with recursive values REQUIRES z.lazy()
|
|
133
|
+
// We also force ZodTypeAny here to break TypeScript circular inference loops
|
|
134
|
+
const inRecordContext = refs.path.includes("additionalProperties");
|
|
135
|
+
// For recursive refs, use ZodTypeAny to avoid TypeScript circular inference errors ("implicitly has type 'any'")
|
|
136
|
+
// User feedback: relying on ZodTypeAny loses type safety. We will try to rely on inference or ZodType<unknown>.
|
|
137
|
+
// However, TS 4.x/5.x often requires explicit type for recursive inferred types.
|
|
138
|
+
// Zod documentation recommends: z.ZodType<MyType> = z.lazy(...)
|
|
139
|
+
// Since we don't have the named type available here easily, we rely on inference by removing the generic.
|
|
140
|
+
const isRecursive = isSameCycle || isForwardRef || refName === refs.currentSchemaName;
|
|
141
|
+
const refType = isRecursive || inRecordContext ? "z.ZodTypeAny" : `typeof ${refName}`;
|
|
142
|
+
// Use deferred/lazy logic if recursive or in a context that requires it (record/catchall)
|
|
143
|
+
if (isRecursive || inRecordContext) {
|
|
144
|
+
// We MUST use z.lazy() for ANY recursive reference, even in named properties given that z.object() is eager.
|
|
145
|
+
// The previous optimization (skipping lazy for named properties) caused TDZ errors because getters on the arg object are evaluated immediately.
|
|
146
|
+
return {
|
|
147
|
+
expression: `z.lazy(() => ${refName})`,
|
|
148
|
+
type: `z.ZodLazy<${refType}>`,
|
|
149
|
+
};
|
|
155
150
|
}
|
|
156
151
|
return { expression: refName, type: refType };
|
|
157
152
|
};
|
|
158
|
-
const addDescribes = (schema, parsed
|
|
153
|
+
const addDescribes = (schema, parsed) => {
|
|
159
154
|
let { expression, type } = parsed;
|
|
160
|
-
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
155
|
+
const meta = {};
|
|
156
|
+
if (schema.$id)
|
|
157
|
+
meta.id = schema.$id;
|
|
158
|
+
if (schema.title)
|
|
159
|
+
meta.title = schema.title;
|
|
160
|
+
if (schema.description)
|
|
161
|
+
meta.description = schema.description;
|
|
162
|
+
if (schema.examples)
|
|
163
|
+
meta.examples = schema.examples;
|
|
164
|
+
if (schema.deprecated)
|
|
165
|
+
meta.deprecated = schema.deprecated;
|
|
166
|
+
// Collect other unknown keywords as metadata if configured
|
|
167
|
+
// This aligns with Zod v4 "Custom metadata is preserved"
|
|
168
|
+
// We can filter out known keywords to find the "unknown" ones.
|
|
169
|
+
// This list needs to be comprehensive to avoid polluting meta with standard logic keywords.
|
|
170
|
+
const knownKeywords = new Set([
|
|
171
|
+
"type",
|
|
172
|
+
"properties",
|
|
173
|
+
"additionalProperties",
|
|
174
|
+
"patternProperties",
|
|
175
|
+
"items",
|
|
176
|
+
"prefixItems",
|
|
177
|
+
"additionalItems",
|
|
178
|
+
"contains",
|
|
179
|
+
"minContains",
|
|
180
|
+
"maxContains",
|
|
181
|
+
"required",
|
|
182
|
+
"enum",
|
|
183
|
+
"const",
|
|
184
|
+
"format",
|
|
185
|
+
"minLength",
|
|
186
|
+
"maxLength",
|
|
187
|
+
"pattern",
|
|
188
|
+
"minimum",
|
|
189
|
+
"maximum",
|
|
190
|
+
"exclusiveMinimum",
|
|
191
|
+
"exclusiveMaximum",
|
|
192
|
+
"multipleOf",
|
|
193
|
+
"if",
|
|
194
|
+
"then",
|
|
195
|
+
"else",
|
|
196
|
+
"allOf",
|
|
197
|
+
"anyOf",
|
|
198
|
+
"oneOf",
|
|
199
|
+
"not",
|
|
200
|
+
"$id",
|
|
201
|
+
"$ref",
|
|
202
|
+
"$dynamicRef",
|
|
203
|
+
"$dynamicAnchor",
|
|
204
|
+
"$schema",
|
|
205
|
+
"$defs",
|
|
206
|
+
"definitions",
|
|
207
|
+
"title",
|
|
208
|
+
"description",
|
|
209
|
+
"default",
|
|
210
|
+
"examples",
|
|
211
|
+
"deprecated",
|
|
212
|
+
"readOnly",
|
|
213
|
+
"writeOnly",
|
|
214
|
+
"contentEncoding",
|
|
215
|
+
"contentMediaType",
|
|
216
|
+
"contentSchema",
|
|
217
|
+
"dependentRequired",
|
|
218
|
+
"dependentSchemas",
|
|
219
|
+
"propertyNames",
|
|
220
|
+
"unevaluatedProperties",
|
|
221
|
+
"unevaluatedItems",
|
|
222
|
+
"nullable",
|
|
223
|
+
"discriminator",
|
|
224
|
+
"errorMessage",
|
|
225
|
+
"externalDocs",
|
|
226
|
+
"__originalIndex",
|
|
227
|
+
]);
|
|
228
|
+
Object.keys(schema).forEach((key) => {
|
|
229
|
+
if (!knownKeywords.has(key)) {
|
|
230
|
+
meta[key] = schema[key];
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
if (Object.keys(meta).length > 0) {
|
|
234
|
+
// Only add .meta() if there is something to add
|
|
235
|
+
// Note: Zod v4 .describe() writes to description too, which meta does too?
|
|
236
|
+
// Zod .describe() sets the description property of the schema def.
|
|
237
|
+
// .meta() is for custom metadata.
|
|
238
|
+
// If strict on description, use .describe().
|
|
239
|
+
// Zod v4: schema.describe("foo") sets description.
|
|
240
|
+
// schema.meta({ ... }) is for other stuff?
|
|
241
|
+
// Actually, Zod documentation says: "Custom metadata is preserved".
|
|
242
|
+
if (meta.description) {
|
|
243
|
+
expression += `.describe(${JSON.stringify(meta.description)})`;
|
|
244
|
+
delete meta.description; // Don't duplicate in meta object if using describe
|
|
245
|
+
}
|
|
173
246
|
if (Object.keys(meta).length > 0) {
|
|
174
247
|
expression += `.meta(${JSON.stringify(meta)})`;
|
|
175
248
|
}
|
|
176
249
|
}
|
|
177
|
-
else if (schema.description) {
|
|
178
|
-
expression += `.describe(${JSON.stringify(schema.description)})`;
|
|
179
|
-
}
|
|
180
250
|
return { expression, type };
|
|
181
251
|
};
|
|
182
252
|
const getOrCreateRefName = (pointer, path, refs) => {
|
|
@@ -214,7 +284,11 @@ const buildNameFromPath = (path, used) => {
|
|
|
214
284
|
.join(""))
|
|
215
285
|
.join("")
|
|
216
286
|
: "Ref";
|
|
217
|
-
|
|
287
|
+
let finalName = base;
|
|
288
|
+
if (!finalName.endsWith("Schema")) {
|
|
289
|
+
finalName += "Schema";
|
|
290
|
+
}
|
|
291
|
+
const sanitized = sanitizeIdentifier(finalName);
|
|
218
292
|
if (!used || !used.has(sanitized))
|
|
219
293
|
return sanitized;
|
|
220
294
|
let counter = 2;
|
|
@@ -283,8 +357,7 @@ const selectParser = (schema, refs) => {
|
|
|
283
357
|
else if (its.a.primitive(schema, "string")) {
|
|
284
358
|
return parseString(schema, refs);
|
|
285
359
|
}
|
|
286
|
-
else if (its.a.primitive(schema, "number") ||
|
|
287
|
-
its.a.primitive(schema, "integer")) {
|
|
360
|
+
else if (its.a.primitive(schema, "number") || its.a.primitive(schema, "integer")) {
|
|
288
361
|
return parseNumber(schema);
|
|
289
362
|
}
|
|
290
363
|
else if (its.a.primitive(schema, "boolean")) {
|
|
@@ -302,7 +375,11 @@ const selectParser = (schema, refs) => {
|
|
|
302
375
|
};
|
|
303
376
|
export const its = {
|
|
304
377
|
an: {
|
|
305
|
-
object: (x) => x.type === "object"
|
|
378
|
+
object: (x) => x.type === "object" ||
|
|
379
|
+
x.properties !== undefined ||
|
|
380
|
+
x.additionalProperties !== undefined ||
|
|
381
|
+
x.patternProperties !== undefined ||
|
|
382
|
+
x.required !== undefined,
|
|
306
383
|
array: (x) => x.type === "array",
|
|
307
384
|
anyOf: (x) => x.anyOf !== undefined,
|
|
308
385
|
allOf: (x) => x.allOf !== undefined,
|
|
@@ -15,8 +15,8 @@ export const parseSimpleDiscriminatedOneOf = (schema, refs) => {
|
|
|
15
15
|
path: [...refs.path, "oneOf", 0],
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
|
-
const expressions = options.map(o => o.expression).join(", ");
|
|
19
|
-
const types = options.map(o => o.type).join(", ");
|
|
18
|
+
const expressions = options.map((o) => o.expression).join(", ");
|
|
19
|
+
const types = options.map((o) => o.type).join(", ");
|
|
20
20
|
return {
|
|
21
21
|
expression: `z.discriminatedUnion("${discriminator}", [${expressions}])`,
|
|
22
22
|
type: `z.ZodDiscriminatedUnion<"${discriminator}", [${types}]>`,
|