@aeye/core 0.1.2 → 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
@@ -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) {
@@ -127,55 +154,516 @@ function strictifySimple(schema, transform) {
127
154
  return schema;
128
155
  }
129
156
  /**
130
- * Override function to modify JSON Schema generation
131
- * to make optional fields accept null values.
157
+ * Converts a Zod schema to JSON Schema V2 with support for different provider formats.
158
+ *
159
+ * This is a custom implementation that recursively inspects Zod schemas and converts them
160
+ * to JSON Schema following the pattern of strictify/strictifySimple.
161
+ *
162
+ * @param schema - The Zod schema to convert
163
+ * @param options - Configuration options for the conversion
164
+ * @returns JSON Schema object compatible with the specified format
165
+ *
166
+ * @example
167
+ * ```typescript
168
+ * const schema = z.object({
169
+ * name: z.string(),
170
+ * age: z.number().optional(),
171
+ * });
172
+ *
173
+ * // For OpenAI strict mode
174
+ * const strictSchema = strictify(schema);
175
+ * const jsonSchema = toJSONSchemaV2(strictSchema, { strict: true, format: 'openai' });
176
+ * ```
177
+ */
178
+ export function toJSONSchema(schema, options) {
179
+ const resolvedOptions = typeof options === 'boolean' ? { strict: options } : options;
180
+ const { strict, format = 'openai' } = resolvedOptions;
181
+ // Currently only OpenAI format is supported
182
+ if (format !== 'openai') {
183
+ throw new Error(`Unsupported format: ${format}. Currently only 'openai' is supported.`);
184
+ }
185
+ const context = {
186
+ root: schema,
187
+ strict,
188
+ format,
189
+ definitions: new Map(),
190
+ definitionSchemas: new Map(),
191
+ refCounter: 0,
192
+ path: [],
193
+ };
194
+ const result = convert(schema, context);
195
+ // Add definitions if any were created
196
+ if (context.definitionSchemas.size > 0) {
197
+ result.$defs = Object.fromEntries(context.definitionSchemas);
198
+ }
199
+ return result;
200
+ }
201
+ /**
202
+ * Main recursive conversion function
132
203
  */
133
- const typeOverride = ({ jsonSchema }) => {
134
- if (jsonSchema.type === 'object') {
135
- const properties = jsonSchema.properties || {};
136
- const propertyKeys = Object.keys(properties);
137
- const required = jsonSchema.required || [];
138
- const optional = propertyKeys.filter((key) => !required.includes(key));
139
- 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
- ],
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
+ }
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
289
+ // Handle ZodOptional
290
+ if (schema instanceof z.ZodOptional) {
291
+ const innerSchema = schema.unwrap();
292
+ const innerJson = convert(innerSchema, context);
293
+ if (context.strict) {
294
+ // In strict mode, optional fields become nullable
295
+ return makeNullable(innerJson);
296
+ }
297
+ else {
298
+ // In non-strict mode, optional fields are just not required
299
+ return innerJson;
300
+ }
301
+ }
302
+ // Handle ZodNullable
303
+ if (schema instanceof z.ZodNullable) {
304
+ const innerSchema = schema.unwrap();
305
+ const innerJson = convert(innerSchema, context);
306
+ return makeNullable(innerJson);
307
+ }
308
+ // Handle ZodObject
309
+ if (schema instanceof z.ZodObject) {
310
+ const properties = {};
311
+ const required = [];
312
+ const shape = schema.shape;
313
+ for (const key in shape) {
314
+ const fieldSchema = shape[key];
315
+ const isRequired = context.strict || !isOptional(fieldSchema);
316
+ properties[key] = convert(fieldSchema, context);
317
+ if (isRequired) {
318
+ required.push(key);
319
+ }
320
+ }
321
+ const result = {
322
+ type: 'object',
323
+ properties,
324
+ required,
325
+ };
326
+ if (context.strict || schema.def.catchall?._zod.def.type === "never") {
327
+ result.additionalProperties = false;
328
+ }
329
+ else if (schema.def.catchall) {
330
+ result.additionalProperties = convert(schema.def.catchall, context);
331
+ }
332
+ return result;
333
+ }
334
+ // Handle ZodArray
335
+ if (schema instanceof z.ZodArray) {
336
+ const result = {
337
+ type: 'array',
338
+ items: convert(schema.element, context),
339
+ };
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;
348
+ }
349
+ // Handle ZodRecord - convert to array of {key, value} pairs in strict mode
350
+ if (schema instanceof z.ZodRecord) {
351
+ if (context.strict) {
352
+ // In strict mode, records become arrays of {key, value} objects
353
+ return {
354
+ type: 'array',
355
+ items: {
356
+ type: 'object',
357
+ properties: {
358
+ key: convert(schema.keyType, context),
359
+ value: convert(schema.valueType, context),
360
+ },
361
+ required: ['key', 'value'],
362
+ additionalProperties: false,
363
+ },
149
364
  };
150
365
  }
151
- jsonSchema.required = propertyKeys;
152
- jsonSchema.additionalProperties = false;
366
+ else {
367
+ // In non-strict mode, use additionalProperties
368
+ return {
369
+ type: 'object',
370
+ propertyNames: convert(schema.keyType, context),
371
+ additionalProperties: convert(schema.valueType, context),
372
+ };
373
+ }
374
+ }
375
+ // Handle ZodUnion
376
+ if (schema instanceof z.ZodUnion) {
377
+ const anyOf = schema.options.map(option => convert(option, context));
378
+ return { anyOf };
153
379
  }
154
- if (jsonSchema.allOf?.length) {
155
- if (jsonSchema.allOf.length === 1) {
156
- if (Object.keys(jsonSchema).length === 1) {
157
- Object.assign(jsonSchema, jsonSchema.allOf[0]);
380
+ // Handle ZodDiscriminatedUnion
381
+ if (schema instanceof z.ZodDiscriminatedUnion) {
382
+ const anyOf = schema.options.map(option => convert(option, context));
383
+ return { anyOf };
384
+ }
385
+ // Handle ZodIntersection - not supported in OpenAI strict mode, convert to anyOf
386
+ if (schema instanceof z.ZodIntersection) {
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 };
395
+ }
396
+ // Handle ZodTuple
397
+ if (schema instanceof z.ZodTuple) {
398
+ // Tuples in JSON Schema can be represented as arrays with prefixItems
399
+ // For simplicity, we'll use an array type
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
+ }
406
+ // If all items are the same type, simplify to a single items schema
407
+ if (items.length > 0 && items.every((item) => JSON.stringify(item) === JSON.stringify(items[0]))) {
408
+ result.items = items[0];
409
+ }
410
+ else {
411
+ if (context.strict) {
412
+ result.items = { anyOf: items };
158
413
  }
159
414
  else {
160
- jsonSchema.anyOf = jsonSchema.allOf;
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--;
161
425
  }
162
- delete jsonSchema.allOf;
426
+ result.minItems = minItems;
427
+ result.maxItems = items.length;
163
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;
437
+ }
438
+ // Handle ZodEnum
439
+ if (schema instanceof z.ZodEnum) {
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);
444
+ return {
445
+ type: values.every((v) => typeof v === 'number')
446
+ ? 'number'
447
+ : values.every((v) => typeof v === 'string')
448
+ ? 'string'
449
+ : undefined,
450
+ enum: values,
451
+ };
452
+ }
453
+ // Handle ZodLiteral
454
+ if (schema instanceof z.ZodLiteral) {
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)));
457
+ return {
458
+ ...(types.length === 1 ? { type: types[0] } : {}),
459
+ ...(values.length === 1 ? { const: values[0] } : { enum: values }),
460
+ };
461
+ }
462
+ // Handle ZodDefault
463
+ if (schema instanceof z.ZodDefault) {
464
+ const innerJson = convert(schema.def.innerType, context);
465
+ innerJson.default = JSON.parse(JSON.stringify(schema.def.defaultValue));
466
+ return innerJson;
467
+ }
468
+ // Handle ZodLazy - don't unwrap here, let convert() handle caching
469
+ // Just return empty schema since ZodLazy should be handled at convert() level
470
+ if (schema instanceof z.ZodLazy) {
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()');
473
+ }
474
+ // Handle ZodCodec
475
+ if (schema instanceof z.ZodCodec) {
476
+ // For codecs, use the input schema for JSON Schema generation
477
+ return convert(schema.def.in, context);
478
+ }
479
+ // Handle ZodPipe (from preprocess)
480
+ if (schema instanceof z.ZodPipe) {
481
+ const innerType = schema.def.in._zod.def.type === "transform" ? schema.def.out : schema.def.in;
482
+ return convert(innerType, context);
164
483
  }
165
- };
484
+ // Handle primitive types
485
+ if (schema instanceof z.ZodString) {
486
+ const result = { type: 'string' };
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;
492
+ }
493
+ if (patterns && patterns.size > 0) {
494
+ result.pattern = Array.from(patterns)[0].source;
495
+ }
496
+ }
497
+ else {
498
+ if (typeof minimum === 'number') {
499
+ result.minLength = minimum;
500
+ }
501
+ if (typeof maximum === 'number') {
502
+ result.maxLength = maximum;
503
+ }
504
+ if (typeof format === 'string') {
505
+ result.format = format;
506
+ }
507
+ if (typeof contentEncoding === 'string') {
508
+ result.contentEncoding = contentEncoding;
509
+ }
510
+ if (patterns) {
511
+ if (patterns.size === 1) {
512
+ result.pattern = Array.from(patterns)[0].source;
513
+ }
514
+ else {
515
+ result.allOf = Array.from(patterns).map((regex) => ({
516
+ type: 'string',
517
+ pattern: regex.source,
518
+ }));
519
+ }
520
+ }
521
+ }
522
+ return result;
523
+ }
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' };
550
+ }
551
+ if (schema instanceof z.ZodBigInt) {
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' };
565
+ }
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' };
577
+ }
578
+ if (schema instanceof z.ZodNull) {
579
+ return { type: 'null' };
580
+ }
581
+ if (schema instanceof z.ZodTemplateLiteral) {
582
+ return { type: 'string', pattern: schema._zod.pattern?.source };
583
+ }
584
+ if (schema instanceof z.ZodUndefined) {
585
+ return { type: 'null' }; // Treat undefined as null in JSON
586
+ }
587
+ if (schema instanceof z.ZodAny) {
588
+ return {}; // Any type has no constraints
589
+ }
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
595
+ }
596
+ // Fallback for unknown types
597
+ console.warn(`Unknown Zod schema type: ${schema.constructor.name}`);
598
+ return {};
599
+ }
166
600
  /**
167
- * Converts a Zod schema to JSON Schema that's compatible with AI schemas.
168
- *
169
- * @param schema - input Zod schema
170
- * @returns - JSON Schema object
601
+ * Makes a JSON Schema nullable by adding null to the type
171
602
  */
172
- export function toJSONSchema(schema, strict) {
173
- return z.toJSONSchema(schema, {
174
- target: 'draft-7',
175
- reused: 'ref',
176
- io: 'input',
177
- unrepresentable: 'any',
178
- override: strict ? typeOverride : undefined,
179
- });
603
+ function makeNullable(schema) {
604
+ if (schema.$ref) {
605
+ // If it's a reference, wrap in anyOf with null
606
+ return {
607
+ anyOf: [
608
+ { $ref: schema.$ref },
609
+ { type: 'null' },
610
+ ],
611
+ };
612
+ }
613
+ if (schema.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
638
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
639
+ if (!types.includes('null')) {
640
+ return {
641
+ ...schema,
642
+ type: [...types, 'null'],
643
+ };
644
+ }
645
+ return schema;
646
+ }
647
+ if (schema.anyOf) {
648
+ // Already has anyOf, add null option if not present
649
+ const hasNull = schema.anyOf.some((s) => s.type === 'null');
650
+ if (!hasNull) {
651
+ return {
652
+ ...schema,
653
+ anyOf: [...schema.anyOf, { type: 'null' }],
654
+ };
655
+ }
656
+ return schema;
657
+ }
658
+ // For complex schemas without type, wrap in anyOf
659
+ return {
660
+ anyOf: [
661
+ schema,
662
+ { type: 'null' },
663
+ ],
664
+ };
665
+ }
666
+ function isOptional(schema) {
667
+ return schema._zod.optin !== undefined;
180
668
  }
181
669
  //# sourceMappingURL=schema.js.map