| 531 | } |
| 532 | |
| 533 | function convertSchema(schema: JSONSchema.JSONSchema | boolean, ctx: ConversionContext): ZodType { |
| 534 | if (typeof schema === "boolean") { |
| 535 | return schema ? z.any() : z.never(); |
| 536 | } |
| 537 | |
| 538 | // Convert base schema first (ignoring composition keywords) |
| 539 | let baseSchema = convertBaseSchema(schema, ctx); |
| 540 | const hasExplicitType = schema.type || schema.enum !== undefined || schema.const !== undefined; |
| 541 | |
| 542 | // Process composition keywords LAST (they can appear together) |
| 543 | // Handle anyOf - wrap base schema with union |
| 544 | if (schema.anyOf && Array.isArray(schema.anyOf)) { |
| 545 | const options = schema.anyOf.map((s) => convertSchema(s, ctx)); |
| 546 | const anyOfUnion = z.union(options as [ZodType, ZodType, ...ZodType[]]); |
| 547 | baseSchema = hasExplicitType ? z.intersection(baseSchema, anyOfUnion) : anyOfUnion; |
| 548 | } |
| 549 | |
| 550 | // Handle oneOf - exclusive union (exactly one must match) |
| 551 | if (schema.oneOf && Array.isArray(schema.oneOf)) { |
| 552 | const options = schema.oneOf.map((s) => convertSchema(s, ctx)); |
| 553 | const oneOfUnion = z.xor(options as [ZodType, ZodType, ...ZodType[]]); |
| 554 | baseSchema = hasExplicitType ? z.intersection(baseSchema, oneOfUnion) : oneOfUnion; |
| 555 | } |
| 556 | |
| 557 | // Handle allOf - wrap base schema with intersection |
| 558 | if (schema.allOf && Array.isArray(schema.allOf)) { |
| 559 | if (schema.allOf.length === 0) { |
| 560 | baseSchema = hasExplicitType ? baseSchema : z.any(); |
| 561 | } else { |
| 562 | let result = hasExplicitType ? baseSchema : convertSchema(schema.allOf[0]!, ctx); |
| 563 | const startIdx = hasExplicitType ? 0 : 1; |
| 564 | for (let i = startIdx; i < schema.allOf.length; i++) { |
| 565 | result = z.intersection(result, convertSchema(schema.allOf[i]!, ctx)); |
| 566 | } |
| 567 | baseSchema = result; |
| 568 | } |
| 569 | } |
| 570 | |
| 571 | // Handle nullable (OpenAPI 3.0) |
| 572 | if (schema.nullable === true && ctx.version === "openapi-3.0") { |
| 573 | baseSchema = z.nullable(baseSchema); |
| 574 | } |
| 575 | |
| 576 | // Handle readOnly |
| 577 | if (schema.readOnly === true) { |
| 578 | baseSchema = z.readonly(baseSchema); |
| 579 | } |
| 580 | |
| 581 | // Apply `default` so it wraps the fully-composed schema. This ensures |
| 582 | // `parse(undefined) -> default` works regardless of which branch of |
| 583 | // `convertBaseSchema` produced the inner schema (enum/const/not/typed/etc.). |
| 584 | if (schema.default !== undefined) { |
| 585 | baseSchema = baseSchema.default(schema.default); |
| 586 | } |
| 587 | |
| 588 | // Collect non-description annotation metadata into the user-supplied |
| 589 | // registry. Description is handled separately below via `.describe()` to |
| 590 | // preserve the contract that `schema.description` reads from globalRegistry. |