@aeye/core 0.2.0 → 0.3.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/dist/schema.js CHANGED
@@ -153,163 +153,6 @@ function strictifySimple(schema, transform) {
153
153
  // For all other types (primitives, etc.), return as-is
154
154
  return schema;
155
155
  }
156
- /**
157
- * Modify a JSON Schema property to make it optional (accept null)
158
- *
159
- * @param prop - input JSON Schema property
160
- * @returns
161
- */
162
- const makeOptional = (prop) => {
163
- if (prop.type) {
164
- const values = Array.isArray(prop.type) ? prop.type : [prop.type];
165
- if (!values.includes('null')) {
166
- prop.type = [...values, 'null'];
167
- }
168
- return prop;
169
- }
170
- else if (prop.$ref) {
171
- // If it's a $ref, wrap it in anyOf with null
172
- return {
173
- anyOf: [
174
- { $ref: prop.$ref },
175
- { type: 'null' }
176
- ],
177
- };
178
- }
179
- else if (prop.allOf) {
180
- // If it's an allOf with a single entry containing a $ref, extract and wrap the $ref
181
- // This handles the common case: { allOf: [{ $ref: "..." }] }
182
- if (prop.allOf.length === 1 && prop.allOf[0].$ref) {
183
- return {
184
- anyOf: [
185
- { $ref: prop.allOf[0].$ref },
186
- { type: 'null' }
187
- ],
188
- };
189
- }
190
- // Otherwise preserve the allOf structure
191
- return {
192
- anyOf: [
193
- { allOf: prop.allOf },
194
- { type: 'null' }
195
- ],
196
- };
197
- }
198
- else {
199
- const { title, description, default: defaultValue, ...rest } = prop;
200
- return {
201
- title,
202
- description,
203
- default: defaultValue,
204
- anyOf: [
205
- rest,
206
- { type: 'null' }
207
- ],
208
- };
209
- }
210
- };
211
- /**
212
- * Override function to modify JSON Schema generation
213
- * to make optional fields accept null values.
214
- */
215
- const typeOverride = ({ zodSchema, jsonSchema, path }) => {
216
- if (jsonSchema.type === 'object' && zodSchema instanceof z.ZodObject) {
217
- const properties = jsonSchema.properties || {};
218
- const propertyKeys = Object.keys(properties);
219
- const required = jsonSchema.required || [];
220
- const optional = propertyKeys.filter((key) => !required.includes(key));
221
- // Add null to optional fields (those not in required array)
222
- for (const key of optional) {
223
- properties[key] = makeOptional(properties[key]);
224
- }
225
- // Check fields that are required - if they use preprocess with optional, add null
226
- for (const key of required) {
227
- const fieldSchema = zodSchema.shape[key];
228
- // Check if this is a ZodPipe (from preprocess) wrapping optional
229
- if (fieldSchema instanceof z.ZodPipe) {
230
- const outputSchema = fieldSchema.def.out;
231
- if (outputSchema instanceof z.ZodOptional) {
232
- const prop = properties[key];
233
- // Skip empty objects - they're placeholders for recursive schemas that will be filled in later
234
- if (Object.keys(prop).length === 0) {
235
- continue;
236
- }
237
- properties[key] = makeOptional(prop);
238
- }
239
- }
240
- }
241
- jsonSchema.required = propertyKeys;
242
- jsonSchema.additionalProperties = false;
243
- }
244
- // Convert allOf with single entry to that entry or anyOf.
245
- // allOf is not supported by some AI schema parsers.
246
- if (jsonSchema.allOf?.length) {
247
- if (jsonSchema.allOf.length === 1) {
248
- if (Object.keys(jsonSchema).length === 1) {
249
- // Delete allOf first, THEN assign the content
250
- const content = jsonSchema.allOf[0];
251
- delete jsonSchema.allOf;
252
- Object.assign(jsonSchema, content);
253
- }
254
- else {
255
- jsonSchema.anyOf = jsonSchema.allOf;
256
- delete jsonSchema.allOf;
257
- }
258
- }
259
- }
260
- // For anyOf with null, convert to type array
261
- if (jsonSchema.anyOf?.length === 2) {
262
- if (jsonSchema.anyOf[1].type === 'null'
263
- && Object.keys(jsonSchema.anyOf[1]).length === 1
264
- && jsonSchema.anyOf[0].type
265
- && !Array.isArray(jsonSchema.anyOf[0].type)) {
266
- const first = jsonSchema.anyOf[0];
267
- const nullableType = [first.type, 'null'];
268
- delete jsonSchema.anyOf;
269
- Object.assign(jsonSchema, first);
270
- // Set type after assign to avoid it being overwritten
271
- jsonSchema.type = nullableType;
272
- }
273
- }
274
- // anyOf in an anyOf - flatten
275
- // BUT: Don't flatten if it would lose $ref or other important properties
276
- if (jsonSchema.anyOf?.length) {
277
- const newAnyOf = [];
278
- for (const subSchema of jsonSchema.anyOf) {
279
- // Check if this sub-schema has a nested anyOf that should be flattened
280
- // Only flatten if:
281
- // 1. The sub-schema has an anyOf property
282
- // 2. The sub-schema has no other properties except anyOf (it's a pure wrapper)
283
- // 3. None of the elements in the nested anyOf are $refs (which should be preserved)
284
- const isPureAnyOfWrapper = subSchema.anyOf?.length && Object.keys(subSchema).length === 1;
285
- const hasRefInNested = subSchema.anyOf?.some((nested) => nested.$ref);
286
- if (isPureAnyOfWrapper && !hasRefInNested) {
287
- // Safe to flatten
288
- newAnyOf.push(...subSchema.anyOf);
289
- }
290
- else {
291
- // Keep as-is
292
- newAnyOf.push(subSchema);
293
- }
294
- }
295
- jsonSchema.anyOf = newAnyOf;
296
- }
297
- };
298
- /**
299
- * Converts a Zod schema to JSON Schema that's compatible with AI schemas.
300
- *
301
- * @param schema - input Zod schema
302
- * @returns - JSON Schema object
303
- */
304
- export function toJSONSchema(schema, strict) {
305
- return z.toJSONSchema(schema, {
306
- target: 'draft-7',
307
- reused: 'ref',
308
- io: 'input',
309
- unrepresentable: 'any',
310
- override: strict ? typeOverride : undefined,
311
- });
312
- }
313
156
  /**
314
157
  * Converts a Zod schema to JSON Schema V2 with support for different provider formats.
315
158
  *
@@ -332,13 +175,15 @@ export function toJSONSchema(schema, strict) {
332
175
  * const jsonSchema = toJSONSchemaV2(strictSchema, { strict: true, format: 'openai' });
333
176
  * ```
334
177
  */
335
- export function toJSONSchemaV2(schema, options) {
336
- const { strict, format = 'openai' } = options;
178
+ export function toJSONSchema(schema, options) {
179
+ const resolvedOptions = typeof options === 'boolean' ? { strict: options } : options;
180
+ const { strict, format = 'openai' } = resolvedOptions;
337
181
  // Currently only OpenAI format is supported
338
182
  if (format !== 'openai') {
339
183
  throw new Error(`Unsupported format: ${format}. Currently only 'openai' is supported.`);
340
184
  }
341
185
  const context = {
186
+ root: schema,
342
187
  strict,
343
188
  format,
344
189
  definitions: new Map(),
@@ -346,7 +191,7 @@ export function toJSONSchemaV2(schema, options) {
346
191
  refCounter: 0,
347
192
  path: [],
348
193
  };
349
- const result = convertSchema(schema, context);
194
+ const result = convert(schema, context);
350
195
  // Add definitions if any were created
351
196
  if (context.definitionSchemas.size > 0) {
352
197
  result.$defs = Object.fromEntries(context.definitionSchemas);
@@ -356,16 +201,95 @@ export function toJSONSchemaV2(schema, options) {
356
201
  /**
357
202
  * Main recursive conversion function
358
203
  */
359
- function convertSchema(schema, context) {
360
- // Check if we've seen this schema before (for recursion)
361
- if (context.definitions.has(schema)) {
362
- const refName = context.definitions.get(schema);
363
- return { $ref: `#/$defs/${refName}` };
204
+ function convert(schema, context) {
205
+ // For lazy schemas, we need to check by aid first to handle .describe()/.optional() wrappers
206
+ // that create new objects but share the same lazy getter
207
+ let cacheKey = schema;
208
+ let unwrappedSchema = schema;
209
+ // Check if this is a lazy schema and extract metadata early
210
+ if (schema instanceof z.ZodLazy) {
211
+ const metadata = (schema instanceof z.ZodType) ? schema.meta() : null;
212
+ const aid = metadata?.aid;
213
+ // If there's an aid, check if we've already seen this aid
214
+ if (aid) {
215
+ for (const [cachedSchema, [js, jsId]] of context.definitions.entries()) {
216
+ if (cachedSchema instanceof z.ZodType) {
217
+ const cachedMeta = cachedSchema.meta();
218
+ if (cachedMeta?.aid === aid && jsId && js) {
219
+ // Found a cached version with the same aid - use it
220
+ if (schema === context.root) {
221
+ return { $ref: `#` };
222
+ }
223
+ if (!context.definitionSchemas.has(jsId)) {
224
+ context.definitionSchemas.set(jsId, { ...js });
225
+ for (const prop in js) {
226
+ delete js[prop];
227
+ }
228
+ js.$ref = `#/$defs/${jsId}`;
229
+ }
230
+ return { $ref: `#/$defs/${jsId}` };
231
+ }
232
+ }
233
+ }
234
+ }
235
+ unwrappedSchema = schema.def.getter();
236
+ }
237
+ else {
238
+ // Check cache by object identity for non-lazy schemas
239
+ const [js, jsId] = context.definitions.get(schema) || [];
240
+ if (jsId && js) {
241
+ if (schema === context.root) {
242
+ return { $ref: `#` };
243
+ }
244
+ if (!context.definitionSchemas.has(jsId)) {
245
+ context.definitionSchemas.set(jsId, { ...js });
246
+ for (const prop in js) {
247
+ delete js[prop];
248
+ }
249
+ js.$ref = `#/$defs/${jsId}`;
250
+ }
251
+ return { $ref: `#/$defs/${jsId}` };
252
+ }
253
+ }
254
+ // Capture metadata from the original schema
255
+ const metadata = {};
256
+ if (schema instanceof z.ZodType) {
257
+ Object.assign(metadata, schema.meta() || {});
258
+ if (!metadata.description && schema.description) {
259
+ metadata.description = schema.description;
260
+ }
364
261
  }
262
+ // If the schema has an 'aid' or 'id' in meta, promote it to a definition
263
+ const id = (metadata.aid ? String(metadata.aid) : 0) || metadata.id || `__schema${context.refCounter++}`;
264
+ const save = !!(metadata.aid || metadata.id) && context.root !== schema;
265
+ // A schema target - will hold either the converted schema or a $ref
266
+ const target = {};
267
+ // Before converting, register this schema to handle recursion
268
+ context.definitions.set(cacheKey, [target, id]);
269
+ // Convert the unwrapped schema
270
+ const result = convertSchema(unwrappedSchema, context);
271
+ Object.assign(result, metadata);
272
+ delete result.aid;
273
+ // Promote it because user requested it or it's recursive
274
+ if (save || context.definitionSchemas.has(id)) {
275
+ context.definitionSchemas.set(id, result);
276
+ target.$ref = `#/$defs/${id}`;
277
+ }
278
+ else {
279
+ // Inline schema
280
+ Object.assign(target, result);
281
+ }
282
+ return target;
283
+ }
284
+ /**
285
+ * Main conversion function
286
+ */
287
+ function convertSchema(schema, context) {
288
+ // TODO: Map, Set, File, ReadOnly, Nan, Catch, Prefault, NonOptional, Transform, Function, Promise, Custom
365
289
  // Handle ZodOptional
366
290
  if (schema instanceof z.ZodOptional) {
367
291
  const innerSchema = schema.unwrap();
368
- const innerJson = convertSchema(innerSchema, context);
292
+ const innerJson = convert(innerSchema, context);
369
293
  if (context.strict) {
370
294
  // In strict mode, optional fields become nullable
371
295
  return makeNullable(innerJson);
@@ -378,56 +302,19 @@ function convertSchema(schema, context) {
378
302
  // Handle ZodNullable
379
303
  if (schema instanceof z.ZodNullable) {
380
304
  const innerSchema = schema.unwrap();
381
- const innerJson = convertSchema(innerSchema, context);
305
+ const innerJson = convert(innerSchema, context);
382
306
  return makeNullable(innerJson);
383
307
  }
384
308
  // Handle ZodObject
385
309
  if (schema instanceof z.ZodObject) {
386
- // Check if this object is recursive (contains self-references through getters)
387
- // If it has an 'aid' in meta, create a definition for it
388
- const meta = schema.meta();
389
- if (meta?.aid && !context.definitions.has(schema)) {
390
- // Register this schema as a definition
391
- const refName = meta.aid;
392
- context.definitions.set(schema, refName);
393
- // Convert the schema
394
- const properties = {};
395
- const required = [];
396
- const shape = schema.shape;
397
- for (const key in shape) {
398
- const fieldSchema = shape[key];
399
- const isOptional = fieldSchema instanceof z.ZodOptional ||
400
- (fieldSchema instanceof z.ZodPipe && fieldSchema.def.out instanceof z.ZodOptional);
401
- properties[key] = convertSchema(fieldSchema, context);
402
- // In strict mode, ALL fields are required (optional ones are nullable)
403
- // In non-strict mode, only non-optional fields are required
404
- if (context.strict || !isOptional) {
405
- required.push(key);
406
- }
407
- }
408
- const schemaJson = {
409
- type: 'object',
410
- properties,
411
- required,
412
- };
413
- if (context.strict) {
414
- schemaJson.additionalProperties = false;
415
- }
416
- addMetadata(schemaJson, schema);
417
- context.definitionSchemas.set(refName, schemaJson);
418
- return { $ref: `#/$defs/${refName}` };
419
- }
420
310
  const properties = {};
421
311
  const required = [];
422
312
  const shape = schema.shape;
423
313
  for (const key in shape) {
424
314
  const fieldSchema = shape[key];
425
- const isOptional = fieldSchema instanceof z.ZodOptional ||
426
- (fieldSchema instanceof z.ZodPipe && fieldSchema.def.out instanceof z.ZodOptional);
427
- properties[key] = convertSchema(fieldSchema, context);
428
- // In strict mode, ALL fields are required (optional ones are nullable)
429
- // In non-strict mode, only non-optional fields are required
430
- if (context.strict || !isOptional) {
315
+ const isRequired = context.strict || !isOptional(fieldSchema);
316
+ properties[key] = convert(fieldSchema, context);
317
+ if (isRequired) {
431
318
  required.push(key);
432
319
  }
433
320
  }
@@ -436,35 +323,40 @@ function convertSchema(schema, context) {
436
323
  properties,
437
324
  required,
438
325
  };
439
- if (context.strict) {
326
+ if (context.strict || schema.def.catchall?._zod.def.type === "never") {
440
327
  result.additionalProperties = false;
441
328
  }
442
- return addMetadata(result, schema);
329
+ else if (schema.def.catchall) {
330
+ result.additionalProperties = convert(schema.def.catchall, context);
331
+ }
332
+ return result;
443
333
  }
444
334
  // Handle ZodArray
445
335
  if (schema instanceof z.ZodArray) {
446
- const items = convertSchema(schema.element, context);
447
336
  const result = {
448
337
  type: 'array',
449
- items,
338
+ items: convert(schema.element, context),
450
339
  };
451
- return addMetadata(result, schema);
340
+ const { minimum, maximum } = schema._zod.bag;
341
+ if (typeof minimum === 'number') {
342
+ result.minItems = minimum;
343
+ }
344
+ if (typeof maximum === 'number') {
345
+ result.maxItems = maximum;
346
+ }
347
+ return result;
452
348
  }
453
349
  // Handle ZodRecord - convert to array of {key, value} pairs in strict mode
454
350
  if (schema instanceof z.ZodRecord) {
455
- const keySchema = schema.keyType || z.string();
456
- const valueSchema = schema.valueType;
457
351
  if (context.strict) {
458
352
  // In strict mode, records become arrays of {key, value} objects
459
- const keyJson = convertSchema(keySchema, context);
460
- const valueJson = convertSchema(valueSchema, context);
461
353
  return {
462
354
  type: 'array',
463
355
  items: {
464
356
  type: 'object',
465
357
  properties: {
466
- key: keyJson,
467
- value: valueJson,
358
+ key: convert(schema.keyType, context),
359
+ value: convert(schema.valueType, context),
468
360
  },
469
361
  required: ['key', 'value'],
470
362
  additionalProperties: false,
@@ -473,198 +365,233 @@ function convertSchema(schema, context) {
473
365
  }
474
366
  else {
475
367
  // In non-strict mode, use additionalProperties
476
- const valueJson = convertSchema(valueSchema, context);
477
368
  return {
478
369
  type: 'object',
479
- additionalProperties: valueJson,
370
+ propertyNames: convert(schema.keyType, context),
371
+ additionalProperties: convert(schema.valueType, context),
480
372
  };
481
373
  }
482
374
  }
483
375
  // Handle ZodUnion
484
376
  if (schema instanceof z.ZodUnion) {
485
- const anyOf = schema.options.map((option) => convertSchema(option, context));
377
+ const anyOf = schema.options.map(option => convert(option, context));
486
378
  return { anyOf };
487
379
  }
488
380
  // Handle ZodDiscriminatedUnion
489
381
  if (schema instanceof z.ZodDiscriminatedUnion) {
490
- const anyOf = schema.options.map((option) => convertSchema(option, context));
382
+ const anyOf = schema.options.map(option => convert(option, context));
491
383
  return { anyOf };
492
384
  }
493
385
  // Handle ZodIntersection - not supported in OpenAI strict mode, convert to anyOf
494
386
  if (schema instanceof z.ZodIntersection) {
495
- if (context.strict && context.format === 'openai') {
496
- // Convert intersection to anyOf for OpenAI compatibility
497
- return {
498
- anyOf: [
499
- convertSchema(schema.def.left, context),
500
- convertSchema(schema.def.right, context),
501
- ],
502
- };
503
- }
504
- // For non-strict, we could try to merge objects, but for now use anyOf
505
- return {
506
- anyOf: [
507
- convertSchema(schema.def.left, context),
508
- convertSchema(schema.def.right, context),
509
- ],
510
- };
387
+ const left = convert(schema.def.left, context);
388
+ const right = convert(schema.def.right, context);
389
+ const allOf = [
390
+ ...(left.allOf && Object.keys(left).length === 1 ? left.allOf : [left]),
391
+ ...(right.allOf && Object.keys(right).length === 1 ? right.allOf : [right]),
392
+ ];
393
+ // TODO merge types to support allOf directly for strict mode
394
+ return context.strict ? { anyOf: allOf } : { allOf };
511
395
  }
512
396
  // Handle ZodTuple
513
397
  if (schema instanceof z.ZodTuple) {
514
398
  // Tuples in JSON Schema can be represented as arrays with prefixItems
515
399
  // For simplicity, we'll use an array type
516
- const items = schema.def.items.map((item) => convertSchema(item, context));
400
+ const items = schema.def.items.map(item => convert(item, context));
401
+ const rest = schema.def.rest ? convert(schema.def.rest, context) : undefined;
402
+ const result = { type: 'array' };
403
+ if (context.strict && rest) {
404
+ items.push(rest);
405
+ }
517
406
  // If all items are the same type, simplify to a single items schema
518
407
  if (items.length > 0 && items.every((item) => JSON.stringify(item) === JSON.stringify(items[0]))) {
519
- return {
520
- type: 'array',
521
- items: items[0],
522
- };
408
+ result.items = items[0];
523
409
  }
524
- // Otherwise, use anyOf for the items
525
- return {
526
- type: 'array',
527
- items: {
528
- anyOf: items,
529
- },
530
- };
410
+ else {
411
+ if (context.strict) {
412
+ result.items = { anyOf: items };
413
+ }
414
+ else {
415
+ result.prefixItems = items;
416
+ }
417
+ }
418
+ if (!context.strict && rest) {
419
+ result.additionalItems = rest;
420
+ }
421
+ if (!rest) {
422
+ let minItems = items.length;
423
+ while (minItems > 0 && isOptional(schema.def.items[minItems - 1])) {
424
+ minItems--;
425
+ }
426
+ result.minItems = minItems;
427
+ result.maxItems = items.length;
428
+ }
429
+ const { minimum, maximum } = schema._zod.bag;
430
+ if (typeof minimum === 'number') {
431
+ result.minItems = minimum;
432
+ }
433
+ if (typeof maximum === 'number') {
434
+ result.maxItems = maximum;
435
+ }
436
+ return result;
531
437
  }
532
438
  // Handle ZodEnum
533
439
  if (schema instanceof z.ZodEnum) {
534
- const result = {
535
- type: 'string',
536
- enum: schema.options,
537
- };
538
- return addMetadata(result, schema);
539
- }
540
- // Handle ZodNativeEnum
541
- if (schema._def?.typeName === 'ZodNativeEnum') {
542
- const enumValues = Object.values(schema._def.values);
440
+ const numericValues = Object.values(schema.def.entries).filter((v) => typeof v === "number");
441
+ const values = Object.entries(schema.def.entries)
442
+ .filter(([k, _]) => numericValues.indexOf(+k) === -1)
443
+ .map(([_, v]) => v);
543
444
  return {
544
- type: typeof enumValues[0] === 'number' ? 'number' : 'string',
545
- enum: enumValues,
445
+ type: values.every((v) => typeof v === 'number')
446
+ ? 'number'
447
+ : values.every((v) => typeof v === 'string')
448
+ ? 'string'
449
+ : undefined,
450
+ enum: values,
546
451
  };
547
452
  }
548
453
  // Handle ZodLiteral
549
454
  if (schema instanceof z.ZodLiteral) {
550
- const value = schema.value;
455
+ const values = Array.from(schema.values).filter(v => v !== undefined && typeof v !== 'function' && typeof v !== 'symbol' && typeof v !== 'bigint');
456
+ const types = Array.from(new Set(values.map(v => v === null ? 'null' : typeof v)));
551
457
  return {
552
- type: typeof value,
553
- const: value,
458
+ ...(types.length === 1 ? { type: types[0] } : {}),
459
+ ...(values.length === 1 ? { const: values[0] } : { enum: values }),
554
460
  };
555
461
  }
556
462
  // Handle ZodDefault
557
463
  if (schema instanceof z.ZodDefault) {
558
- const innerJson = convertSchema(schema.def.innerType, context);
559
- innerJson.default = schema.def.defaultValue;
464
+ const innerJson = convert(schema.def.innerType, context);
465
+ innerJson.default = JSON.parse(JSON.stringify(schema.def.defaultValue));
560
466
  return innerJson;
561
467
  }
562
- // Handle ZodLazy
468
+ // Handle ZodLazy - don't unwrap here, let convert() handle caching
469
+ // Just return empty schema since ZodLazy should be handled at convert() level
563
470
  if (schema instanceof z.ZodLazy) {
564
- // Check if schema has an 'aid' in meta for custom definition name
565
- const meta = schema.meta();
566
- const refName = meta?.aid || `LazySchema${context.refCounter++}`;
567
- context.definitions.set(schema, refName);
568
- // Get the actual schema
569
- const actualSchema = schema.def.getter();
570
- const schemaJson = convertSchema(actualSchema, context);
571
- context.definitionSchemas.set(refName, schemaJson);
572
- return { $ref: `#/$defs/${refName}` };
471
+ // This shouldn't be reached because convert() handles ZodLazy before calling convertSchema
472
+ throw new Error('ZodLazy should be handled in convert(), not convertSchema()');
573
473
  }
574
474
  // Handle ZodCodec
575
475
  if (schema instanceof z.ZodCodec) {
576
476
  // For codecs, use the input schema for JSON Schema generation
577
- return convertSchema(schema.def.in, context);
477
+ return convert(schema.def.in, context);
578
478
  }
579
479
  // Handle ZodPipe (from preprocess)
580
480
  if (schema instanceof z.ZodPipe) {
581
- // For pipes, use the output schema if in strict mode (since strictify creates pipes for optional->nullable conversion)
582
- // Otherwise use input schema
583
- if (context.strict && schema.def.out instanceof z.ZodOptional) {
584
- // This is from strictify - the output is optional, so we want nullable
585
- const innerSchema = schema.def.out.unwrap();
586
- const innerJson = convertSchema(innerSchema, context);
587
- return makeNullable(innerJson);
588
- }
589
- return convertSchema(schema.def.in, context);
590
- }
591
- // Handle ZodTransform (from z.transform())
592
- if (schema._def?.typeName === 'ZodTransform') {
593
- // Use the input schema for JSON Schema generation
594
- return convertSchema(schema._def.schema, context);
481
+ const innerType = schema.def.in._zod.def.type === "transform" ? schema.def.out : schema.def.in;
482
+ return convert(innerType, context);
595
483
  }
596
484
  // Handle primitive types
597
485
  if (schema instanceof z.ZodString) {
598
486
  const result = { type: 'string' };
599
- // Add string constraints if present
600
- const checks = schema._def.checks || [];
601
- for (const check of checks) {
602
- if (check.kind === 'regex') {
603
- result.pattern = check.regex.source;
487
+ const { minimum, maximum, format, patterns, contentEncoding } = schema._zod.bag;
488
+ if (context.strict) {
489
+ const strictFormats = ['date-time', 'time', 'date', 'duration', 'email', 'hostname', 'ipv4', 'ipv6', 'uuid'];
490
+ if (typeof format === 'string' && strictFormats.includes(format)) {
491
+ result.format = format;
604
492
  }
605
- else if (check.kind === 'email') {
606
- result.format = 'email';
493
+ if (patterns && patterns.size > 0) {
494
+ result.pattern = Array.from(patterns)[0].source;
607
495
  }
608
- else if (check.kind === 'url') {
609
- result.format = 'uri';
496
+ }
497
+ else {
498
+ if (typeof minimum === 'number') {
499
+ result.minLength = minimum;
610
500
  }
611
- else if (check.kind === 'uuid') {
612
- result.format = 'uuid';
501
+ if (typeof maximum === 'number') {
502
+ result.maxLength = maximum;
613
503
  }
614
- else if (check.kind === 'datetime') {
615
- result.format = 'date-time';
504
+ if (typeof format === 'string') {
505
+ result.format = format;
616
506
  }
617
- }
618
- return addMetadata(result, schema);
619
- }
620
- if (schema instanceof z.ZodNumber) {
621
- const result = { type: 'number' };
622
- // Add number constraints if present
623
- const checks = schema._def.checks || [];
624
- for (const check of checks) {
625
- if (check.kind === 'min') {
626
- if (check.inclusive) {
627
- result.minimum = check.value;
628
- }
629
- else {
630
- result.exclusiveMinimum = check.value;
631
- }
507
+ if (typeof contentEncoding === 'string') {
508
+ result.contentEncoding = contentEncoding;
632
509
  }
633
- else if (check.kind === 'max') {
634
- if (check.inclusive) {
635
- result.maximum = check.value;
510
+ if (patterns) {
511
+ if (patterns.size === 1) {
512
+ result.pattern = Array.from(patterns)[0].source;
636
513
  }
637
514
  else {
638
- result.exclusiveMaximum = check.value;
515
+ result.allOf = Array.from(patterns).map((regex) => ({
516
+ type: 'string',
517
+ pattern: regex.source,
518
+ }));
639
519
  }
640
520
  }
641
- else if (check.kind === 'multipleOf') {
642
- result.multipleOf = check.value;
643
- }
644
521
  }
645
- return addMetadata(result, schema);
522
+ return result;
646
523
  }
647
- if (schema instanceof z.ZodBoolean) {
648
- return addMetadata({ type: 'boolean' }, schema);
524
+ if (schema instanceof z.ZodNumber) {
525
+ const result = { type: 'number' };
526
+ // Add number constraints if present
527
+ const { minimum, maximum, format, multipleOf, exclusiveMaximum, exclusiveMinimum } = schema._zod.bag;
528
+ if (typeof format === 'string' && format.includes("int")) {
529
+ result.type = 'integer';
530
+ }
531
+ if (typeof exclusiveMinimum === 'number') {
532
+ result.exclusiveMinimum = exclusiveMinimum;
533
+ }
534
+ else if (typeof minimum === 'number') {
535
+ result.minimum = minimum;
536
+ }
537
+ if (typeof exclusiveMaximum === 'number') {
538
+ result.exclusiveMaximum = exclusiveMaximum;
539
+ }
540
+ else if (typeof maximum === 'number') {
541
+ result.maximum = maximum;
542
+ }
543
+ if (typeof multipleOf === 'number') {
544
+ result.multipleOf = multipleOf;
545
+ }
546
+ return result;
547
+ }
548
+ if (schema instanceof z.ZodBoolean || schema instanceof z.ZodSuccess) {
549
+ return { type: 'boolean' };
649
550
  }
650
551
  if (schema instanceof z.ZodBigInt) {
651
- return addMetadata({ type: 'integer' }, schema);
552
+ return { type: 'integer' };
553
+ }
554
+ if (schema instanceof z.ZodDate || schema instanceof z.ZodISODateTime) {
555
+ return { type: 'string', format: 'date-time' };
556
+ }
557
+ if (schema instanceof z.ZodISODate) {
558
+ return { type: 'string', format: 'date' };
559
+ }
560
+ if (schema instanceof z.ZodISOTime) {
561
+ return { type: 'string', format: 'time' };
562
+ }
563
+ if (schema instanceof z.ZodISODuration) {
564
+ return { type: 'string', format: 'duration' };
652
565
  }
653
- if (schema instanceof z.ZodDate) {
654
- return addMetadata({ type: 'string', format: 'date-time' }, schema);
566
+ if (schema instanceof z.ZodEmail) {
567
+ return { type: 'string', format: 'email' };
568
+ }
569
+ if (schema instanceof z.ZodIPv4) {
570
+ return { type: 'string', format: 'ipv4' };
571
+ }
572
+ if (schema instanceof z.ZodIPv6) {
573
+ return { type: 'string', format: 'ipv6' };
574
+ }
575
+ if (schema instanceof z.ZodUUID) {
576
+ return { type: 'string', format: 'uuid' };
655
577
  }
656
578
  if (schema instanceof z.ZodNull) {
657
579
  return { type: 'null' };
658
580
  }
581
+ if (schema instanceof z.ZodTemplateLiteral) {
582
+ return { type: 'string', pattern: schema._zod.pattern?.source };
583
+ }
659
584
  if (schema instanceof z.ZodUndefined) {
660
585
  return { type: 'null' }; // Treat undefined as null in JSON
661
586
  }
662
587
  if (schema instanceof z.ZodAny) {
663
588
  return {}; // Any type has no constraints
664
589
  }
665
- // Handle ZodEffects (transformations, refinements) - use inner schema
666
- if (schema._def?.typeName === 'ZodEffects') {
667
- return convertSchema(schema._def.schema, context);
590
+ if (schema instanceof z.ZodUnknown) {
591
+ return {}; // Unknown type has no constraints
592
+ }
593
+ if (schema instanceof z.ZodTransform) {
594
+ return {}; // Transforms are not represented in JSON Schema
668
595
  }
669
596
  // Fallback for unknown types
670
597
  console.warn(`Unknown Zod schema type: ${schema.constructor.name}`);
@@ -684,7 +611,30 @@ function makeNullable(schema) {
684
611
  };
685
612
  }
686
613
  if (schema.type) {
687
- // Add null to the type
614
+ // Check if schema has type-specific properties that require anyOf structure
615
+ // Arrays have 'items', objects have 'properties', etc.
616
+ const hasTypeSpecificProps = schema.items !== undefined ||
617
+ schema.properties !== undefined ||
618
+ schema.additionalProperties !== undefined ||
619
+ schema.patternProperties !== undefined ||
620
+ schema.minItems !== undefined ||
621
+ schema.maxItems !== undefined ||
622
+ schema.minProperties !== undefined ||
623
+ schema.maxProperties !== undefined;
624
+ // If there are type-specific properties, must use anyOf to separate type schemas
625
+ if (hasTypeSpecificProps) {
626
+ // Extract type-specific properties based on the type
627
+ const { type, description, ...rest } = schema;
628
+ const baseType = Array.isArray(type) ? type[0] : type;
629
+ return {
630
+ anyOf: [
631
+ { type: baseType, ...rest },
632
+ { type: 'null' },
633
+ ],
634
+ ...(description ? { description } : {}),
635
+ };
636
+ }
637
+ // For simple types without type-specific properties, can use array notation
688
638
  const types = Array.isArray(schema.type) ? schema.type : [schema.type];
689
639
  if (!types.includes('null')) {
690
640
  return {
@@ -713,17 +663,7 @@ function makeNullable(schema) {
713
663
  ],
714
664
  };
715
665
  }
716
- /**
717
- * Adds metadata (description, meta) from Zod schema to JSON Schema
718
- */
719
- function addMetadata(jsonSchema, zodSchema) {
720
- if (zodSchema.description) {
721
- jsonSchema.description = zodSchema.description;
722
- }
723
- const meta = zodSchema.meta();
724
- if (meta?.title) {
725
- jsonSchema.title = meta.title;
726
- }
727
- return jsonSchema;
666
+ function isOptional(schema) {
667
+ return schema._zod.optin !== undefined;
728
668
  }
729
669
  //# sourceMappingURL=schema.js.map