@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.
Files changed (53) hide show
  1. package/.github/RELEASE_SETUP.md +120 -0
  2. package/.github/TOOLING_GUIDE.md +169 -0
  3. package/.github/dependabot.yml +52 -0
  4. package/.github/workflows/ci.yml +33 -0
  5. package/.github/workflows/release.yml +12 -4
  6. package/.github/workflows/security.yml +40 -0
  7. package/.husky/commit-msg +1 -0
  8. package/.husky/pre-commit +1 -0
  9. package/.lintstagedrc.json +3 -0
  10. package/.prettierrc +20 -0
  11. package/AGENTS.md +7 -0
  12. package/CHANGELOG.md +13 -4
  13. package/README.md +9 -9
  14. package/commitlint.config.js +24 -0
  15. package/createIndex.ts +4 -4
  16. package/dist/cli.js +3 -4
  17. package/dist/core/analyzeSchema.js +28 -5
  18. package/dist/core/emitZod.js +11 -4
  19. package/dist/generators/generateBundle.js +67 -92
  20. package/dist/parsers/parseAllOf.js +11 -12
  21. package/dist/parsers/parseAnyOf.js +2 -2
  22. package/dist/parsers/parseArray.js +38 -12
  23. package/dist/parsers/parseMultipleType.js +2 -2
  24. package/dist/parsers/parseNumber.js +44 -102
  25. package/dist/parsers/parseObject.js +138 -393
  26. package/dist/parsers/parseOneOf.js +57 -100
  27. package/dist/parsers/parseSchema.js +132 -55
  28. package/dist/parsers/parseSimpleDiscriminatedOneOf.js +2 -2
  29. package/dist/parsers/parseString.js +113 -253
  30. package/dist/types/Types.d.ts +22 -1
  31. package/dist/types/core/analyzeSchema.d.ts +1 -0
  32. package/dist/types/generators/generateBundle.d.ts +1 -1
  33. package/dist/utils/cliTools.js +1 -2
  34. package/dist/utils/esmEmitter.js +6 -2
  35. package/dist/utils/extractInlineObject.js +1 -3
  36. package/dist/utils/jsdocs.js +1 -4
  37. package/dist/utils/liftInlineObjects.js +76 -15
  38. package/dist/utils/resolveRef.js +35 -10
  39. package/dist/utils/schemaRepresentation.js +35 -66
  40. package/dist/zodToJsonSchema.js +1 -2
  41. package/docs/IMPROVEMENT-PLAN.md +30 -12
  42. package/docs/ZOD-V4-RECURSIVE-TYPE-LIMITATIONS.md +70 -25
  43. package/docs/proposals/allof-required-merging.md +10 -4
  44. package/docs/proposals/bundle-refactor.md +10 -4
  45. package/docs/proposals/discriminated-union-with-default.md +18 -14
  46. package/docs/proposals/inline-object-lifting.md +15 -5
  47. package/docs/proposals/ref-anchor-support.md +11 -0
  48. package/output.txt +67 -0
  49. package/package.json +18 -5
  50. package/scripts/generateWorkflowSchema.ts +5 -14
  51. package/scripts/regenerate_bundle.ts +25 -0
  52. package/tsc_output.txt +542 -0
  53. 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 !== 'object' || member === null)
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 === 'object' && resolved.schema !== null) {
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 the constant value from a property schema.
117
- * Returns the string value if it's a const or single-element enum, undefined otherwise.
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 getConstValue = (prop) => {
120
- if (prop.const !== undefined && typeof prop.const === 'string') {
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 === 1 &&
126
- typeof prop.enum[0] === 'string') {
127
- return prop.enum[0];
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 === 'object' &&
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 === 'string')) {
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 !== 'object' || opt === null)
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 === 'object' && resolved.schema !== null) {
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 !== 'object') {
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 === 'object' && prop !== null && (prop.$ref || prop.$dynamicRef)) {
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 !== 'object' || prop === null) {
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 = getConstValue(prop);
209
+ const constValue = getDiscriminatorValues(prop);
219
210
  if (constValue !== undefined) {
220
- if (constValuesSet.has(constValue)) {
221
- isValidDiscriminator = false; // Duplicate value found
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 (constValues.length === resolvedOptions.length) {
249
- return { type: 'full', key };
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
- constValues.length === resolvedOptions.length - 1) {
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: 'withDefault', key, defaultIndex, constValues };
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 === 'full') {
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
- // Note: 'withDefault' case (discriminated union with catch-all) cannot be optimized
297
- // in Zod v4 because ZodDiscriminatedUnion cannot be nested inside ZodUnion at the type level.
298
- // The runtime would work, but the types wouldn't match, causing compile errors.
299
- // So we fall through to the regular union handling below.
300
- // Fallback: Standard z.union
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 extracted = extractInlineObject(s, refs, [...refs.path, "oneOf", i]);
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
- // Build the union types for the SchemaRepresentation
331
- const unionTypes = parsedSchemas.map(r => r.type).join(", ");
332
- const unionExpression = `z.union([${parsedSchemas.map(r => r.expression).join(", ")}])`;
333
- if (refs.strictOneOf) {
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: unionExpression,
362
- // Use readonly tuple for union type annotations (required for recursive type inference)
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, { ...refs, currentBaseUri: baseUri, dynamicAnchors });
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, { ...refs, currentBaseUri: baseUri, dynamicAnchors });
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
- const refType = `typeof ${refName}`;
127
- // For same-cycle refs, check if we need special handling
128
- if (isSameCycle || isForwardRef) {
129
- // Check context: are we inside an object property where getters work?
130
- // IMPORTANT: additionalProperties becomes z.record() which does NOT support getters
131
- // Only named properties (properties, patternProperties) can use getters
132
- const inNamedProperty = refs.path.includes("properties") ||
133
- refs.path.includes("patternProperties");
134
- // additionalProperties becomes z.record() value - getters don't work there
135
- // Per Zod issue #4881: z.record() with recursive values REQUIRES z.lazy()
136
- const inRecordContext = refs.path.includes("additionalProperties");
137
- // Self-recursion in named object properties: use direct ref (getter handles deferred eval)
138
- const isSelfRecursion = refName === refs.currentSchemaName;
139
- if (inNamedProperty && isSelfRecursion) {
140
- return { expression: refName, type: refType };
141
- }
142
- // Cross-schema refs in named object properties within same cycle: use direct ref
143
- // The getter in parseObject.ts will handle deferred evaluation
144
- if (inNamedProperty && isSameCycle && !isForwardRef) {
145
- return { expression: refName, type: refType };
146
- }
147
- // z.record() values with recursive refs MUST use z.lazy() (Colin confirmed in #4881)
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, refs) => {
153
+ const addDescribes = (schema, parsed) => {
159
154
  let { expression, type } = parsed;
160
- // Use .meta() for richer metadata when withMeta is enabled
161
- if (refs?.withMeta) {
162
- const meta = {};
163
- if (schema.$id)
164
- meta.id = schema.$id;
165
- if (schema.title)
166
- meta.title = schema.title;
167
- if (schema.description)
168
- meta.description = schema.description;
169
- if (schema.examples)
170
- meta.examples = schema.examples;
171
- if (schema.deprecated)
172
- meta.deprecated = schema.deprecated;
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
- const sanitized = sanitizeIdentifier(base || "Ref");
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}]>`,