@aeye/core 0.1.2 → 0.2.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
@@ -1,22 +1,5 @@
1
1
  import z from 'zod';
2
2
  /**
3
- * Transfer description and metadata from source Zod schema to target Zod schema
4
- *
5
- * @param target - target Zod schema
6
- * @param source - source Zod schema
7
- * @returns
8
- */
9
- function transferMetadata(target, source) {
10
- if (source.description) {
11
- target = target.describe(source.description);
12
- }
13
- const meta = source.meta();
14
- if (meta) {
15
- target = target.meta(meta);
16
- }
17
- return target;
18
- }
19
- /**
20
3
  * Recursively transforms a Zod schema to support strict mode.
21
4
  *
22
5
  * @param schema - input Zod schema
@@ -43,6 +26,44 @@ export function strictify(schema) {
43
26
  return transform(schema);
44
27
  }
45
28
  /**
29
+ * Transfer description and metadata from source Zod schema to target Zod schema
30
+ *
31
+ * @param target - target Zod schema
32
+ * @param source - source Zod schema
33
+ * @returns
34
+ */
35
+ function transferMetadata(target, source) {
36
+ if (source.description) {
37
+ target = target.describe(source.description);
38
+ }
39
+ const meta = source.meta();
40
+ if (meta) {
41
+ target = target.meta(meta);
42
+ }
43
+ return target;
44
+ }
45
+ /**
46
+ * Extracts the input schema from a transformed schema (codec or preprocess).
47
+ * For preprocess with optional, we need the nullable version for strict mode input.
48
+ * This is needed when building input schemas that contain transformed fields.
49
+ */
50
+ function getInputSchema(schema) {
51
+ if (schema instanceof z.ZodCodec) {
52
+ return schema.def.in;
53
+ }
54
+ // For ZodPipe (which preprocess creates), check if the output is optional
55
+ // If so, we need to return a nullable version for the input schema
56
+ if (schema instanceof z.ZodPipe) {
57
+ const outputSchema = schema.def.out;
58
+ if (outputSchema instanceof z.ZodOptional) {
59
+ // The input from preprocess is already set up to handle null->undefined conversion
60
+ // So we just need to make sure the base type accepts null
61
+ return z.nullable(outputSchema.unwrap());
62
+ }
63
+ }
64
+ return schema;
65
+ }
66
+ /**
46
67
  * Recursively transforms a Zod schema to support strict mode.
47
68
  *
48
69
  * @param schema - input Zod schema
@@ -51,11 +72,14 @@ export function strictify(schema) {
51
72
  function strictifySimple(schema, transform) {
52
73
  // Handle ZodOptional - get the inner schema and make it optional
53
74
  if (schema instanceof z.ZodOptional) {
54
- const transformed = transform(schema.unwrap());
55
- return transferMetadata(z.codec(transformed.nullable(), transformed.optional(), {
56
- decode: (val) => val === null ? undefined : val,
57
- encode: (val) => val === undefined ? null : val,
58
- }), schema);
75
+ const innerSchema = schema.unwrap();
76
+ const transformed = transform(innerSchema);
77
+ // Check if the inner schema is nullable
78
+ // If it's nullable, don't convert null to undefined
79
+ const isNullable = innerSchema instanceof z.ZodNullable;
80
+ // Use a preprocess to convert null to undefined only for non-nullable schemas
81
+ // This avoids issues with codec output validation in recursive schemas
82
+ return transferMetadata(z.preprocess((val) => (!isNullable && val === null) ? undefined : val, transformed.optional()), schema);
59
83
  }
60
84
  // Handle ZodObject
61
85
  if (schema instanceof z.ZodObject) {
@@ -68,8 +92,8 @@ function strictifySimple(schema, transform) {
68
92
  // Handle ZodCodec
69
93
  if (schema instanceof z.ZodCodec) {
70
94
  return transferMetadata(z.codec(transform(schema.def.in), transform(schema.def.out), {
71
- encode: schema.def.transform,
72
- decode: schema.def.reverseTransform,
95
+ decode: schema.def.transform,
96
+ encode: schema.def.reverseTransform,
73
97
  }), schema);
74
98
  }
75
99
  // Handle ZodPipe
@@ -86,9 +110,12 @@ function strictifySimple(schema, transform) {
86
110
  }
87
111
  // Handle ZodRecord
88
112
  if (schema instanceof z.ZodRecord) {
89
- const key = schema.keyType ? transform(schema.keyType) : z.string();
90
- const value = transform(schema.valueType);
91
- return transferMetadata(z.codec(z.array(z.object({ key, value })), z.record(key, value), {
113
+ const keyTransformed = schema.keyType ? transform(schema.keyType) : z.string();
114
+ const valueTransformed = transform(schema.valueType);
115
+ // For the input schema (array of {key, value}), use the input side of any codecs
116
+ const key = getInputSchema(keyTransformed);
117
+ const value = getInputSchema(valueTransformed);
118
+ return transferMetadata(z.codec(z.array(z.object({ key, value })), z.record(keyTransformed, valueTransformed), {
92
119
  decode: (arr) => {
93
120
  const record = {};
94
121
  for (const { key, value } of arr) {
@@ -126,41 +153,146 @@ function strictifySimple(schema, transform) {
126
153
  // For all other types (primitives, etc.), return as-is
127
154
  return schema;
128
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
+ };
129
211
  /**
130
212
  * Override function to modify JSON Schema generation
131
213
  * to make optional fields accept null values.
132
214
  */
133
- const typeOverride = ({ jsonSchema }) => {
134
- if (jsonSchema.type === 'object') {
215
+ const typeOverride = ({ zodSchema, jsonSchema, path }) => {
216
+ if (jsonSchema.type === 'object' && zodSchema instanceof z.ZodObject) {
135
217
  const properties = jsonSchema.properties || {};
136
218
  const propertyKeys = Object.keys(properties);
137
219
  const required = jsonSchema.required || [];
138
220
  const optional = propertyKeys.filter((key) => !required.includes(key));
221
+ // Add null to optional fields (those not in required array)
139
222
  for (const key of optional) {
140
- const { title, description, default: defaultValue, ...rest } = properties[key];
141
- properties[key] = {
142
- title,
143
- description,
144
- default: defaultValue,
145
- anyOf: [
146
- rest,
147
- { type: 'null' }
148
- ],
149
- };
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
+ }
150
240
  }
151
241
  jsonSchema.required = propertyKeys;
152
242
  jsonSchema.additionalProperties = false;
153
243
  }
244
+ // Convert allOf with single entry to that entry or anyOf.
245
+ // allOf is not supported by some AI schema parsers.
154
246
  if (jsonSchema.allOf?.length) {
155
247
  if (jsonSchema.allOf.length === 1) {
156
248
  if (Object.keys(jsonSchema).length === 1) {
157
- Object.assign(jsonSchema, jsonSchema.allOf[0]);
249
+ // Delete allOf first, THEN assign the content
250
+ const content = jsonSchema.allOf[0];
251
+ delete jsonSchema.allOf;
252
+ Object.assign(jsonSchema, content);
158
253
  }
159
254
  else {
160
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);
161
293
  }
162
- delete jsonSchema.allOf;
163
294
  }
295
+ jsonSchema.anyOf = newAnyOf;
164
296
  }
165
297
  };
166
298
  /**
@@ -178,4 +310,420 @@ export function toJSONSchema(schema, strict) {
178
310
  override: strict ? typeOverride : undefined,
179
311
  });
180
312
  }
313
+ /**
314
+ * Converts a Zod schema to JSON Schema V2 with support for different provider formats.
315
+ *
316
+ * This is a custom implementation that recursively inspects Zod schemas and converts them
317
+ * to JSON Schema following the pattern of strictify/strictifySimple.
318
+ *
319
+ * @param schema - The Zod schema to convert
320
+ * @param options - Configuration options for the conversion
321
+ * @returns JSON Schema object compatible with the specified format
322
+ *
323
+ * @example
324
+ * ```typescript
325
+ * const schema = z.object({
326
+ * name: z.string(),
327
+ * age: z.number().optional(),
328
+ * });
329
+ *
330
+ * // For OpenAI strict mode
331
+ * const strictSchema = strictify(schema);
332
+ * const jsonSchema = toJSONSchemaV2(strictSchema, { strict: true, format: 'openai' });
333
+ * ```
334
+ */
335
+ export function toJSONSchemaV2(schema, options) {
336
+ const { strict, format = 'openai' } = options;
337
+ // Currently only OpenAI format is supported
338
+ if (format !== 'openai') {
339
+ throw new Error(`Unsupported format: ${format}. Currently only 'openai' is supported.`);
340
+ }
341
+ const context = {
342
+ strict,
343
+ format,
344
+ definitions: new Map(),
345
+ definitionSchemas: new Map(),
346
+ refCounter: 0,
347
+ path: [],
348
+ };
349
+ const result = convertSchema(schema, context);
350
+ // Add definitions if any were created
351
+ if (context.definitionSchemas.size > 0) {
352
+ result.$defs = Object.fromEntries(context.definitionSchemas);
353
+ }
354
+ return result;
355
+ }
356
+ /**
357
+ * Main recursive conversion function
358
+ */
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}` };
364
+ }
365
+ // Handle ZodOptional
366
+ if (schema instanceof z.ZodOptional) {
367
+ const innerSchema = schema.unwrap();
368
+ const innerJson = convertSchema(innerSchema, context);
369
+ if (context.strict) {
370
+ // In strict mode, optional fields become nullable
371
+ return makeNullable(innerJson);
372
+ }
373
+ else {
374
+ // In non-strict mode, optional fields are just not required
375
+ return innerJson;
376
+ }
377
+ }
378
+ // Handle ZodNullable
379
+ if (schema instanceof z.ZodNullable) {
380
+ const innerSchema = schema.unwrap();
381
+ const innerJson = convertSchema(innerSchema, context);
382
+ return makeNullable(innerJson);
383
+ }
384
+ // Handle ZodObject
385
+ 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
+ const properties = {};
421
+ const required = [];
422
+ const shape = schema.shape;
423
+ for (const key in shape) {
424
+ 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) {
431
+ required.push(key);
432
+ }
433
+ }
434
+ const result = {
435
+ type: 'object',
436
+ properties,
437
+ required,
438
+ };
439
+ if (context.strict) {
440
+ result.additionalProperties = false;
441
+ }
442
+ return addMetadata(result, schema);
443
+ }
444
+ // Handle ZodArray
445
+ if (schema instanceof z.ZodArray) {
446
+ const items = convertSchema(schema.element, context);
447
+ const result = {
448
+ type: 'array',
449
+ items,
450
+ };
451
+ return addMetadata(result, schema);
452
+ }
453
+ // Handle ZodRecord - convert to array of {key, value} pairs in strict mode
454
+ if (schema instanceof z.ZodRecord) {
455
+ const keySchema = schema.keyType || z.string();
456
+ const valueSchema = schema.valueType;
457
+ if (context.strict) {
458
+ // In strict mode, records become arrays of {key, value} objects
459
+ const keyJson = convertSchema(keySchema, context);
460
+ const valueJson = convertSchema(valueSchema, context);
461
+ return {
462
+ type: 'array',
463
+ items: {
464
+ type: 'object',
465
+ properties: {
466
+ key: keyJson,
467
+ value: valueJson,
468
+ },
469
+ required: ['key', 'value'],
470
+ additionalProperties: false,
471
+ },
472
+ };
473
+ }
474
+ else {
475
+ // In non-strict mode, use additionalProperties
476
+ const valueJson = convertSchema(valueSchema, context);
477
+ return {
478
+ type: 'object',
479
+ additionalProperties: valueJson,
480
+ };
481
+ }
482
+ }
483
+ // Handle ZodUnion
484
+ if (schema instanceof z.ZodUnion) {
485
+ const anyOf = schema.options.map((option) => convertSchema(option, context));
486
+ return { anyOf };
487
+ }
488
+ // Handle ZodDiscriminatedUnion
489
+ if (schema instanceof z.ZodDiscriminatedUnion) {
490
+ const anyOf = schema.options.map((option) => convertSchema(option, context));
491
+ return { anyOf };
492
+ }
493
+ // Handle ZodIntersection - not supported in OpenAI strict mode, convert to anyOf
494
+ 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
+ };
511
+ }
512
+ // Handle ZodTuple
513
+ if (schema instanceof z.ZodTuple) {
514
+ // Tuples in JSON Schema can be represented as arrays with prefixItems
515
+ // For simplicity, we'll use an array type
516
+ const items = schema.def.items.map((item) => convertSchema(item, context));
517
+ // If all items are the same type, simplify to a single items schema
518
+ if (items.length > 0 && items.every((item) => JSON.stringify(item) === JSON.stringify(items[0]))) {
519
+ return {
520
+ type: 'array',
521
+ items: items[0],
522
+ };
523
+ }
524
+ // Otherwise, use anyOf for the items
525
+ return {
526
+ type: 'array',
527
+ items: {
528
+ anyOf: items,
529
+ },
530
+ };
531
+ }
532
+ // Handle ZodEnum
533
+ 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);
543
+ return {
544
+ type: typeof enumValues[0] === 'number' ? 'number' : 'string',
545
+ enum: enumValues,
546
+ };
547
+ }
548
+ // Handle ZodLiteral
549
+ if (schema instanceof z.ZodLiteral) {
550
+ const value = schema.value;
551
+ return {
552
+ type: typeof value,
553
+ const: value,
554
+ };
555
+ }
556
+ // Handle ZodDefault
557
+ if (schema instanceof z.ZodDefault) {
558
+ const innerJson = convertSchema(schema.def.innerType, context);
559
+ innerJson.default = schema.def.defaultValue;
560
+ return innerJson;
561
+ }
562
+ // Handle ZodLazy
563
+ 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}` };
573
+ }
574
+ // Handle ZodCodec
575
+ if (schema instanceof z.ZodCodec) {
576
+ // For codecs, use the input schema for JSON Schema generation
577
+ return convertSchema(schema.def.in, context);
578
+ }
579
+ // Handle ZodPipe (from preprocess)
580
+ 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);
595
+ }
596
+ // Handle primitive types
597
+ if (schema instanceof z.ZodString) {
598
+ 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;
604
+ }
605
+ else if (check.kind === 'email') {
606
+ result.format = 'email';
607
+ }
608
+ else if (check.kind === 'url') {
609
+ result.format = 'uri';
610
+ }
611
+ else if (check.kind === 'uuid') {
612
+ result.format = 'uuid';
613
+ }
614
+ else if (check.kind === 'datetime') {
615
+ result.format = 'date-time';
616
+ }
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
+ }
632
+ }
633
+ else if (check.kind === 'max') {
634
+ if (check.inclusive) {
635
+ result.maximum = check.value;
636
+ }
637
+ else {
638
+ result.exclusiveMaximum = check.value;
639
+ }
640
+ }
641
+ else if (check.kind === 'multipleOf') {
642
+ result.multipleOf = check.value;
643
+ }
644
+ }
645
+ return addMetadata(result, schema);
646
+ }
647
+ if (schema instanceof z.ZodBoolean) {
648
+ return addMetadata({ type: 'boolean' }, schema);
649
+ }
650
+ if (schema instanceof z.ZodBigInt) {
651
+ return addMetadata({ type: 'integer' }, schema);
652
+ }
653
+ if (schema instanceof z.ZodDate) {
654
+ return addMetadata({ type: 'string', format: 'date-time' }, schema);
655
+ }
656
+ if (schema instanceof z.ZodNull) {
657
+ return { type: 'null' };
658
+ }
659
+ if (schema instanceof z.ZodUndefined) {
660
+ return { type: 'null' }; // Treat undefined as null in JSON
661
+ }
662
+ if (schema instanceof z.ZodAny) {
663
+ return {}; // Any type has no constraints
664
+ }
665
+ // Handle ZodEffects (transformations, refinements) - use inner schema
666
+ if (schema._def?.typeName === 'ZodEffects') {
667
+ return convertSchema(schema._def.schema, context);
668
+ }
669
+ // Fallback for unknown types
670
+ console.warn(`Unknown Zod schema type: ${schema.constructor.name}`);
671
+ return {};
672
+ }
673
+ /**
674
+ * Makes a JSON Schema nullable by adding null to the type
675
+ */
676
+ function makeNullable(schema) {
677
+ if (schema.$ref) {
678
+ // If it's a reference, wrap in anyOf with null
679
+ return {
680
+ anyOf: [
681
+ { $ref: schema.$ref },
682
+ { type: 'null' },
683
+ ],
684
+ };
685
+ }
686
+ if (schema.type) {
687
+ // Add null to the type
688
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
689
+ if (!types.includes('null')) {
690
+ return {
691
+ ...schema,
692
+ type: [...types, 'null'],
693
+ };
694
+ }
695
+ return schema;
696
+ }
697
+ if (schema.anyOf) {
698
+ // Already has anyOf, add null option if not present
699
+ const hasNull = schema.anyOf.some((s) => s.type === 'null');
700
+ if (!hasNull) {
701
+ return {
702
+ ...schema,
703
+ anyOf: [...schema.anyOf, { type: 'null' }],
704
+ };
705
+ }
706
+ return schema;
707
+ }
708
+ // For complex schemas without type, wrap in anyOf
709
+ return {
710
+ anyOf: [
711
+ schema,
712
+ { type: 'null' },
713
+ ],
714
+ };
715
+ }
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;
728
+ }
181
729
  //# sourceMappingURL=schema.js.map