fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: add Zod module detection

Lubos 3fbc04bf 2b3b6459

+2122 -20
+1 -1
examples/openapi-ts-nuxt/package.json
··· 16 16 "nuxt": "3.14.1592", 17 17 "vue": "3.5.13", 18 18 "vue-router": "4.5.0", 19 - "zod": "3.25.0" 19 + "zod": "3.25.3" 20 20 }, 21 21 "devDependencies": { 22 22 "vite": "6.2.7"
+1 -1
examples/openapi-ts-sample/package.json
··· 18 18 "react": "19.0.0", 19 19 "react-dom": "19.0.0", 20 20 "valibot": "1.1.0", 21 - "zod": "3.25.0" 21 + "zod": "3.25.3" 22 22 }, 23 23 "devDependencies": { 24 24 "@config/vite-base": "workspace:*",
+1 -1
packages/openapi-ts-tests/package.json
··· 57 57 "typescript": "5.8.3", 58 58 "valibot": "1.1.0", 59 59 "vue": "3.5.13", 60 - "zod": "3.25.0" 60 + "zod": "3.25.3" 61 61 } 62 62 }
+1 -1
packages/openapi-ts-tests/test/openapi-ts.config.ts
··· 228 228 { 229 229 // case: 'snake_case', 230 230 // comments: false, 231 - // compatibilityVersion: 4, 231 + compatibilityVersion: 4, 232 232 dates: { 233 233 offset: true, 234 234 },
+1034
packages/openapi-ts/src/plugins/zod/mini/plugin.ts
··· 1 + import ts from 'typescript'; 2 + 3 + import { compiler } from '../../../compiler'; 4 + import { deduplicateSchema } from '../../../ir/schema'; 5 + import type { IR } from '../../../ir/types'; 6 + import { buildName } from '../../../openApi/shared/utils/name'; 7 + import { refToName } from '../../../utils/ref'; 8 + import { numberRegExp } from '../../../utils/regexp'; 9 + import { identifiers, zodId } from '../constants'; 10 + import { exportZodSchema } from '../export'; 11 + import { getZodModule } from '../shared/module'; 12 + import type { ZodSchema } from '../shared/types'; 13 + import type { ZodPlugin } from '../types'; 14 + import { operationToZodSchema } from '../v3/operation'; 15 + 16 + interface SchemaWithType<T extends Required<IR.SchemaObject>['type']> 17 + extends Omit<IR.SchemaObject, 'type'> { 18 + type: Extract<Required<IR.SchemaObject>['type'], T>; 19 + } 20 + 21 + export type State = { 22 + circularReferenceTracker: Array<string>; 23 + hasCircularReference: boolean; 24 + }; 25 + 26 + const arrayTypeToZodSchema = ({ 27 + plugin, 28 + schema, 29 + state, 30 + }: { 31 + plugin: ZodPlugin['Instance']; 32 + schema: SchemaWithType<'array'>; 33 + state: State; 34 + }): ts.CallExpression => { 35 + const functionName = compiler.propertyAccessExpression({ 36 + expression: identifiers.z, 37 + name: identifiers.array, 38 + }); 39 + 40 + let arrayExpression: ts.CallExpression | undefined; 41 + 42 + if (!schema.items) { 43 + arrayExpression = compiler.callExpression({ 44 + functionName, 45 + parameters: [ 46 + unknownTypeToZodSchema({ 47 + schema: { 48 + type: 'unknown', 49 + }, 50 + }), 51 + ], 52 + }); 53 + } else { 54 + schema = deduplicateSchema({ schema }); 55 + 56 + // at least one item is guaranteed 57 + const itemExpressions = schema.items!.map( 58 + (item) => 59 + schemaToZodSchema({ 60 + plugin, 61 + schema: item, 62 + state, 63 + }).expression, 64 + ); 65 + 66 + if (itemExpressions.length === 1) { 67 + arrayExpression = compiler.callExpression({ 68 + functionName, 69 + parameters: itemExpressions, 70 + }); 71 + } else { 72 + if (schema.logicalOperator === 'and') { 73 + // TODO: parser - handle intersection 74 + // return compiler.typeArrayNode( 75 + // compiler.typeIntersectionNode({ types: itemExpressions }), 76 + // ); 77 + } 78 + 79 + arrayExpression = compiler.callExpression({ 80 + functionName: compiler.propertyAccessExpression({ 81 + expression: identifiers.z, 82 + name: identifiers.array, 83 + }), 84 + parameters: [ 85 + compiler.callExpression({ 86 + functionName: compiler.propertyAccessExpression({ 87 + expression: identifiers.z, 88 + name: identifiers.union, 89 + }), 90 + parameters: [ 91 + compiler.arrayLiteralExpression({ 92 + elements: itemExpressions, 93 + }), 94 + ], 95 + }), 96 + ], 97 + }); 98 + } 99 + } 100 + 101 + if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { 102 + arrayExpression = compiler.callExpression({ 103 + functionName: compiler.propertyAccessExpression({ 104 + expression: arrayExpression, 105 + name: identifiers.length, 106 + }), 107 + parameters: [compiler.valueToExpression({ value: schema.minItems })], 108 + }); 109 + } else { 110 + if (schema.minItems !== undefined) { 111 + arrayExpression = compiler.callExpression({ 112 + functionName: compiler.propertyAccessExpression({ 113 + expression: arrayExpression, 114 + name: identifiers.min, 115 + }), 116 + parameters: [compiler.valueToExpression({ value: schema.minItems })], 117 + }); 118 + } 119 + 120 + if (schema.maxItems !== undefined) { 121 + arrayExpression = compiler.callExpression({ 122 + functionName: compiler.propertyAccessExpression({ 123 + expression: arrayExpression, 124 + name: identifiers.max, 125 + }), 126 + parameters: [compiler.valueToExpression({ value: schema.maxItems })], 127 + }); 128 + } 129 + } 130 + 131 + return arrayExpression; 132 + }; 133 + 134 + const booleanTypeToZodSchema = ({ 135 + schema, 136 + }: { 137 + schema: SchemaWithType<'boolean'>; 138 + }) => { 139 + if (typeof schema.const === 'boolean') { 140 + const expression = compiler.callExpression({ 141 + functionName: compiler.propertyAccessExpression({ 142 + expression: identifiers.z, 143 + name: identifiers.literal, 144 + }), 145 + parameters: [compiler.ots.boolean(schema.const)], 146 + }); 147 + return expression; 148 + } 149 + 150 + const expression = compiler.callExpression({ 151 + functionName: compiler.propertyAccessExpression({ 152 + expression: identifiers.z, 153 + name: identifiers.boolean, 154 + }), 155 + }); 156 + return expression; 157 + }; 158 + 159 + const enumTypeToZodSchema = ({ 160 + schema, 161 + }: { 162 + schema: SchemaWithType<'enum'>; 163 + }): ts.CallExpression => { 164 + const enumMembers: Array<ts.LiteralExpression> = []; 165 + 166 + let isNullable = false; 167 + 168 + for (const item of schema.items ?? []) { 169 + // Zod supports only string enums 170 + if (item.type === 'string' && typeof item.const === 'string') { 171 + enumMembers.push( 172 + compiler.stringLiteral({ 173 + text: item.const, 174 + }), 175 + ); 176 + } else if (item.type === 'null' || item.const === null) { 177 + isNullable = true; 178 + } 179 + } 180 + 181 + if (!enumMembers.length) { 182 + return unknownTypeToZodSchema({ 183 + schema: { 184 + type: 'unknown', 185 + }, 186 + }); 187 + } 188 + 189 + let enumExpression = compiler.callExpression({ 190 + functionName: compiler.propertyAccessExpression({ 191 + expression: identifiers.z, 192 + name: identifiers.enum, 193 + }), 194 + parameters: [ 195 + compiler.arrayLiteralExpression({ 196 + elements: enumMembers, 197 + multiLine: false, 198 + }), 199 + ], 200 + }); 201 + 202 + if (isNullable) { 203 + enumExpression = compiler.callExpression({ 204 + functionName: compiler.propertyAccessExpression({ 205 + expression: enumExpression, 206 + name: identifiers.nullable, 207 + }), 208 + }); 209 + } 210 + 211 + return enumExpression; 212 + }; 213 + 214 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 215 + const neverTypeToZodSchema = (_props: { schema: SchemaWithType<'never'> }) => { 216 + const expression = compiler.callExpression({ 217 + functionName: compiler.propertyAccessExpression({ 218 + expression: identifiers.z, 219 + name: identifiers.never, 220 + }), 221 + }); 222 + return expression; 223 + }; 224 + 225 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 226 + const nullTypeToZodSchema = (_props: { schema: SchemaWithType<'null'> }) => { 227 + const expression = compiler.callExpression({ 228 + functionName: compiler.propertyAccessExpression({ 229 + expression: identifiers.z, 230 + name: identifiers.null, 231 + }), 232 + }); 233 + return expression; 234 + }; 235 + 236 + const numberParameter = ({ 237 + isBigInt, 238 + value, 239 + }: { 240 + isBigInt: boolean; 241 + value: unknown; 242 + }) => { 243 + const expression = compiler.valueToExpression({ value }); 244 + 245 + if ( 246 + isBigInt && 247 + (typeof value === 'bigint' || 248 + typeof value === 'number' || 249 + typeof value === 'string' || 250 + typeof value === 'boolean') 251 + ) { 252 + return compiler.callExpression({ 253 + functionName: 'BigInt', 254 + parameters: [expression], 255 + }); 256 + } 257 + 258 + return expression; 259 + }; 260 + 261 + const numberTypeToZodSchema = ({ 262 + schema, 263 + }: { 264 + schema: SchemaWithType<'integer' | 'number'>; 265 + }) => { 266 + const isBigInt = schema.type === 'integer' && schema.format === 'int64'; 267 + 268 + if (typeof schema.const === 'number') { 269 + // TODO: parser - handle bigint constants 270 + const expression = compiler.callExpression({ 271 + functionName: compiler.propertyAccessExpression({ 272 + expression: identifiers.z, 273 + name: identifiers.literal, 274 + }), 275 + parameters: [compiler.ots.number(schema.const)], 276 + }); 277 + return expression; 278 + } 279 + 280 + let numberExpression = compiler.callExpression({ 281 + functionName: isBigInt 282 + ? compiler.propertyAccessExpression({ 283 + expression: compiler.propertyAccessExpression({ 284 + expression: identifiers.z, 285 + name: identifiers.coerce, 286 + }), 287 + name: identifiers.bigint, 288 + }) 289 + : compiler.propertyAccessExpression({ 290 + expression: identifiers.z, 291 + name: identifiers.number, 292 + }), 293 + }); 294 + 295 + if (!isBigInt && schema.type === 'integer') { 296 + numberExpression = compiler.callExpression({ 297 + functionName: compiler.propertyAccessExpression({ 298 + expression: numberExpression, 299 + name: identifiers.int, 300 + }), 301 + }); 302 + } 303 + 304 + if (schema.exclusiveMinimum !== undefined) { 305 + numberExpression = compiler.callExpression({ 306 + functionName: compiler.propertyAccessExpression({ 307 + expression: numberExpression, 308 + name: identifiers.gt, 309 + }), 310 + parameters: [ 311 + numberParameter({ isBigInt, value: schema.exclusiveMinimum }), 312 + ], 313 + }); 314 + } else if (schema.minimum !== undefined) { 315 + numberExpression = compiler.callExpression({ 316 + functionName: compiler.propertyAccessExpression({ 317 + expression: numberExpression, 318 + name: identifiers.gte, 319 + }), 320 + parameters: [numberParameter({ isBigInt, value: schema.minimum })], 321 + }); 322 + } 323 + 324 + if (schema.exclusiveMaximum !== undefined) { 325 + numberExpression = compiler.callExpression({ 326 + functionName: compiler.propertyAccessExpression({ 327 + expression: numberExpression, 328 + name: identifiers.lt, 329 + }), 330 + parameters: [ 331 + numberParameter({ isBigInt, value: schema.exclusiveMaximum }), 332 + ], 333 + }); 334 + } else if (schema.maximum !== undefined) { 335 + numberExpression = compiler.callExpression({ 336 + functionName: compiler.propertyAccessExpression({ 337 + expression: numberExpression, 338 + name: identifiers.lte, 339 + }), 340 + parameters: [numberParameter({ isBigInt, value: schema.maximum })], 341 + }); 342 + } 343 + 344 + return numberExpression; 345 + }; 346 + 347 + const objectTypeToZodSchema = ({ 348 + plugin, 349 + schema, 350 + state, 351 + }: { 352 + plugin: ZodPlugin['Instance']; 353 + schema: SchemaWithType<'object'>; 354 + state: State; 355 + }): { 356 + anyType: string; 357 + expression: ts.CallExpression; 358 + } => { 359 + // TODO: parser - handle constants 360 + const properties: Array<ts.PropertyAssignment> = []; 361 + 362 + const required = schema.required ?? []; 363 + 364 + for (const name in schema.properties) { 365 + const property = schema.properties[name]!; 366 + const isRequired = required.includes(name); 367 + 368 + const propertyExpression = schemaToZodSchema({ 369 + optional: !isRequired, 370 + plugin, 371 + schema: property, 372 + state, 373 + }).expression; 374 + 375 + numberRegExp.lastIndex = 0; 376 + let propertyName; 377 + if (numberRegExp.test(name)) { 378 + // For numeric literals, we'll handle negative numbers by using a string literal 379 + // instead of trying to use a PrefixUnaryExpression 380 + propertyName = name.startsWith('-') 381 + ? ts.factory.createStringLiteral(name) 382 + : ts.factory.createNumericLiteral(name); 383 + } else { 384 + propertyName = name; 385 + } 386 + // TODO: parser - abstract safe property name logic 387 + if ( 388 + ((name.match(/^[0-9]/) && name.match(/\D+/g)) || name.match(/\W/g)) && 389 + !name.startsWith("'") && 390 + !name.endsWith("'") 391 + ) { 392 + propertyName = `'${name}'`; 393 + } 394 + properties.push( 395 + compiler.propertyAssignment({ 396 + initializer: propertyExpression, 397 + name: propertyName, 398 + }), 399 + ); 400 + } 401 + 402 + if ( 403 + schema.additionalProperties && 404 + schema.additionalProperties.type === 'object' && 405 + !Object.keys(properties).length 406 + ) { 407 + const zodSchema = schemaToZodSchema({ 408 + plugin, 409 + schema: schema.additionalProperties, 410 + state, 411 + }).expression; 412 + const expression = compiler.callExpression({ 413 + functionName: compiler.propertyAccessExpression({ 414 + expression: identifiers.z, 415 + name: identifiers.record, 416 + }), 417 + parameters: [zodSchema], 418 + }); 419 + return { 420 + anyType: 'AnyZodObject', 421 + expression, 422 + }; 423 + } 424 + 425 + const expression = compiler.callExpression({ 426 + functionName: compiler.propertyAccessExpression({ 427 + expression: identifiers.z, 428 + name: identifiers.object, 429 + }), 430 + parameters: [ts.factory.createObjectLiteralExpression(properties, true)], 431 + }); 432 + return { 433 + anyType: 'AnyZodObject', 434 + expression, 435 + }; 436 + }; 437 + 438 + const stringTypeToZodSchema = ({ 439 + plugin, 440 + schema, 441 + }: { 442 + plugin: ZodPlugin['Instance']; 443 + schema: SchemaWithType<'string'>; 444 + }) => { 445 + if (typeof schema.const === 'string') { 446 + const expression = compiler.callExpression({ 447 + functionName: compiler.propertyAccessExpression({ 448 + expression: identifiers.z, 449 + name: identifiers.literal, 450 + }), 451 + parameters: [compiler.ots.string(schema.const)], 452 + }); 453 + return expression; 454 + } 455 + 456 + let stringExpression = compiler.callExpression({ 457 + functionName: compiler.propertyAccessExpression({ 458 + expression: identifiers.z, 459 + name: identifiers.string, 460 + }), 461 + }); 462 + 463 + if (schema.format) { 464 + switch (schema.format) { 465 + case 'date-time': 466 + stringExpression = compiler.callExpression({ 467 + functionName: compiler.propertyAccessExpression({ 468 + expression: stringExpression, 469 + name: identifiers.datetime, 470 + }), 471 + parameters: plugin.config.dates.offset 472 + ? [ 473 + compiler.objectExpression({ 474 + obj: [ 475 + { 476 + key: 'offset', 477 + value: true, 478 + }, 479 + ], 480 + }), 481 + ] 482 + : [], 483 + }); 484 + break; 485 + case 'ipv4': 486 + case 'ipv6': 487 + stringExpression = compiler.callExpression({ 488 + functionName: compiler.propertyAccessExpression({ 489 + expression: stringExpression, 490 + name: identifiers.ip, 491 + }), 492 + }); 493 + break; 494 + case 'uri': 495 + stringExpression = compiler.callExpression({ 496 + functionName: compiler.propertyAccessExpression({ 497 + expression: stringExpression, 498 + name: identifiers.url, 499 + }), 500 + }); 501 + break; 502 + case 'date': 503 + case 'email': 504 + case 'time': 505 + case 'uuid': 506 + stringExpression = compiler.callExpression({ 507 + functionName: compiler.propertyAccessExpression({ 508 + expression: stringExpression, 509 + name: compiler.identifier({ text: schema.format }), 510 + }), 511 + }); 512 + break; 513 + } 514 + } 515 + 516 + if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { 517 + stringExpression = compiler.callExpression({ 518 + functionName: compiler.propertyAccessExpression({ 519 + expression: stringExpression, 520 + name: identifiers.length, 521 + }), 522 + parameters: [compiler.valueToExpression({ value: schema.minLength })], 523 + }); 524 + } else { 525 + if (schema.minLength !== undefined) { 526 + stringExpression = compiler.callExpression({ 527 + functionName: compiler.propertyAccessExpression({ 528 + expression: stringExpression, 529 + name: identifiers.min, 530 + }), 531 + parameters: [compiler.valueToExpression({ value: schema.minLength })], 532 + }); 533 + } 534 + 535 + if (schema.maxLength !== undefined) { 536 + stringExpression = compiler.callExpression({ 537 + functionName: compiler.propertyAccessExpression({ 538 + expression: stringExpression, 539 + name: identifiers.max, 540 + }), 541 + parameters: [compiler.valueToExpression({ value: schema.maxLength })], 542 + }); 543 + } 544 + } 545 + 546 + if (schema.pattern) { 547 + stringExpression = compiler.callExpression({ 548 + functionName: compiler.propertyAccessExpression({ 549 + expression: stringExpression, 550 + name: identifiers.regex, 551 + }), 552 + parameters: [compiler.regularExpressionLiteral({ text: schema.pattern })], 553 + }); 554 + } 555 + 556 + return stringExpression; 557 + }; 558 + 559 + const tupleTypeToZodSchema = ({ 560 + plugin, 561 + schema, 562 + state, 563 + }: { 564 + plugin: ZodPlugin['Instance']; 565 + schema: SchemaWithType<'tuple'>; 566 + state: State; 567 + }) => { 568 + if (schema.const && Array.isArray(schema.const)) { 569 + const tupleElements = schema.const.map((value) => 570 + compiler.callExpression({ 571 + functionName: compiler.propertyAccessExpression({ 572 + expression: identifiers.z, 573 + name: identifiers.literal, 574 + }), 575 + parameters: [compiler.valueToExpression({ value })], 576 + }), 577 + ); 578 + const expression = compiler.callExpression({ 579 + functionName: compiler.propertyAccessExpression({ 580 + expression: identifiers.z, 581 + name: identifiers.tuple, 582 + }), 583 + parameters: [ 584 + compiler.arrayLiteralExpression({ 585 + elements: tupleElements, 586 + }), 587 + ], 588 + }); 589 + return expression; 590 + } 591 + 592 + const tupleElements: Array<ts.Expression> = []; 593 + 594 + for (const item of schema.items ?? []) { 595 + tupleElements.push( 596 + schemaToZodSchema({ 597 + plugin, 598 + schema: item, 599 + state, 600 + }).expression, 601 + ); 602 + } 603 + 604 + const expression = compiler.callExpression({ 605 + functionName: compiler.propertyAccessExpression({ 606 + expression: identifiers.z, 607 + name: identifiers.tuple, 608 + }), 609 + parameters: [ 610 + compiler.arrayLiteralExpression({ 611 + elements: tupleElements, 612 + }), 613 + ], 614 + }); 615 + return expression; 616 + }; 617 + 618 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 619 + const undefinedTypeToZodSchema = (_props: { 620 + schema: SchemaWithType<'undefined'>; 621 + }) => { 622 + const expression = compiler.callExpression({ 623 + functionName: compiler.propertyAccessExpression({ 624 + expression: identifiers.z, 625 + name: identifiers.undefined, 626 + }), 627 + }); 628 + return expression; 629 + }; 630 + 631 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 632 + const unknownTypeToZodSchema = (_props: { 633 + schema: SchemaWithType<'unknown'>; 634 + }) => { 635 + const expression = compiler.callExpression({ 636 + functionName: compiler.propertyAccessExpression({ 637 + expression: identifiers.z, 638 + name: identifiers.unknown, 639 + }), 640 + }); 641 + return expression; 642 + }; 643 + 644 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 645 + const voidTypeToZodSchema = (_props: { schema: SchemaWithType<'void'> }) => { 646 + const expression = compiler.callExpression({ 647 + functionName: compiler.propertyAccessExpression({ 648 + expression: identifiers.z, 649 + name: identifiers.void, 650 + }), 651 + }); 652 + return expression; 653 + }; 654 + 655 + const schemaTypeToZodSchema = ({ 656 + plugin, 657 + schema, 658 + state, 659 + }: { 660 + plugin: ZodPlugin['Instance']; 661 + schema: IR.SchemaObject; 662 + state: State; 663 + }): { 664 + anyType?: string; 665 + expression: ts.Expression; 666 + } => { 667 + switch (schema.type as Required<IR.SchemaObject>['type']) { 668 + case 'array': 669 + return { 670 + expression: arrayTypeToZodSchema({ 671 + plugin, 672 + schema: schema as SchemaWithType<'array'>, 673 + state, 674 + }), 675 + }; 676 + case 'boolean': 677 + return { 678 + expression: booleanTypeToZodSchema({ 679 + schema: schema as SchemaWithType<'boolean'>, 680 + }), 681 + }; 682 + case 'enum': 683 + return { 684 + expression: enumTypeToZodSchema({ 685 + schema: schema as SchemaWithType<'enum'>, 686 + }), 687 + }; 688 + case 'integer': 689 + case 'number': 690 + return { 691 + expression: numberTypeToZodSchema({ 692 + schema: schema as SchemaWithType<'integer' | 'number'>, 693 + }), 694 + }; 695 + case 'never': 696 + return { 697 + expression: neverTypeToZodSchema({ 698 + schema: schema as SchemaWithType<'never'>, 699 + }), 700 + }; 701 + case 'null': 702 + return { 703 + expression: nullTypeToZodSchema({ 704 + schema: schema as SchemaWithType<'null'>, 705 + }), 706 + }; 707 + case 'object': 708 + return objectTypeToZodSchema({ 709 + plugin, 710 + schema: schema as SchemaWithType<'object'>, 711 + state, 712 + }); 713 + case 'string': 714 + return { 715 + expression: stringTypeToZodSchema({ 716 + plugin, 717 + schema: schema as SchemaWithType<'string'>, 718 + }), 719 + }; 720 + case 'tuple': 721 + return { 722 + expression: tupleTypeToZodSchema({ 723 + plugin, 724 + schema: schema as SchemaWithType<'tuple'>, 725 + state, 726 + }), 727 + }; 728 + case 'undefined': 729 + return { 730 + expression: undefinedTypeToZodSchema({ 731 + schema: schema as SchemaWithType<'undefined'>, 732 + }), 733 + }; 734 + case 'unknown': 735 + return { 736 + expression: unknownTypeToZodSchema({ 737 + schema: schema as SchemaWithType<'unknown'>, 738 + }), 739 + }; 740 + case 'void': 741 + return { 742 + expression: voidTypeToZodSchema({ 743 + schema: schema as SchemaWithType<'void'>, 744 + }), 745 + }; 746 + } 747 + }; 748 + 749 + export const schemaToZodSchema = ({ 750 + optional, 751 + plugin, 752 + schema, 753 + state, 754 + }: { 755 + /** 756 + * Accept `optional` to handle optional object properties. We can't handle 757 + * this inside the object function because `.optional()` must come before 758 + * `.default()` which is handled in this function. 759 + */ 760 + optional?: boolean; 761 + plugin: ZodPlugin['Instance']; 762 + schema: IR.SchemaObject; 763 + state: State; 764 + }): ZodSchema => { 765 + const file = plugin.context.file({ id: zodId })!; 766 + 767 + let zodSchema: Partial<ZodSchema> = {}; 768 + 769 + if (schema.$ref) { 770 + const isCircularReference = state.circularReferenceTracker.includes( 771 + schema.$ref, 772 + ); 773 + state.circularReferenceTracker.push(schema.$ref); 774 + 775 + const id = plugin.api.getId({ type: 'ref', value: schema.$ref }); 776 + 777 + if (isCircularReference) { 778 + const expression = file.addNodeReference(id, { 779 + factory: (text) => compiler.identifier({ text }), 780 + }); 781 + zodSchema.expression = compiler.callExpression({ 782 + functionName: compiler.propertyAccessExpression({ 783 + expression: identifiers.z, 784 + name: identifiers.lazy, 785 + }), 786 + parameters: [ 787 + compiler.arrowFunction({ 788 + statements: [compiler.returnStatement({ expression })], 789 + }), 790 + ], 791 + }); 792 + state.hasCircularReference = true; 793 + } else if (!file.getName(id)) { 794 + // if $ref hasn't been processed yet, inline it to avoid the 795 + // "Block-scoped variable used before its declaration." error 796 + // this could be (maybe?) fixed by reshuffling the generation order 797 + const ref = plugin.context.resolveIrRef<IR.SchemaObject>(schema.$ref); 798 + handleComponent({ 799 + id: schema.$ref, 800 + plugin, 801 + schema: ref, 802 + state, 803 + }); 804 + } 805 + 806 + if (!isCircularReference) { 807 + const expression = file.addNodeReference(id, { 808 + factory: (text) => compiler.identifier({ text }), 809 + }); 810 + zodSchema.expression = expression; 811 + } 812 + 813 + state.circularReferenceTracker.pop(); 814 + } else if (schema.type) { 815 + const zSchema = schemaTypeToZodSchema({ plugin, schema, state }); 816 + zodSchema.expression = zSchema.expression; 817 + zodSchema.typeName = zSchema.anyType; 818 + 819 + if (plugin.config.metadata && schema.description) { 820 + zodSchema.expression = compiler.callExpression({ 821 + functionName: compiler.propertyAccessExpression({ 822 + expression: zodSchema.expression, 823 + name: identifiers.describe, 824 + }), 825 + parameters: [compiler.stringLiteral({ text: schema.description })], 826 + }); 827 + } 828 + } else if (schema.items) { 829 + schema = deduplicateSchema({ schema }); 830 + 831 + if (schema.items) { 832 + const itemTypes = schema.items.map( 833 + (item) => 834 + schemaToZodSchema({ 835 + plugin, 836 + schema: item, 837 + state, 838 + }).expression, 839 + ); 840 + 841 + if (schema.logicalOperator === 'and') { 842 + const firstSchema = schema.items[0]!; 843 + // we want to add an intersection, but not every schema can use the same API. 844 + // if the first item contains another array or not an object, we cannot use 845 + // `.merge()` as that does not exist on `.union()` and non-object schemas. 846 + if ( 847 + firstSchema.logicalOperator === 'or' || 848 + (firstSchema.type && firstSchema.type !== 'object') 849 + ) { 850 + zodSchema.expression = compiler.callExpression({ 851 + functionName: compiler.propertyAccessExpression({ 852 + expression: identifiers.z, 853 + name: identifiers.intersection, 854 + }), 855 + parameters: itemTypes, 856 + }); 857 + } else { 858 + zodSchema.expression = itemTypes[0]; 859 + itemTypes.slice(1).forEach((item) => { 860 + zodSchema.expression = compiler.callExpression({ 861 + functionName: compiler.propertyAccessExpression({ 862 + expression: zodSchema.expression!, 863 + name: identifiers.and, 864 + }), 865 + parameters: [item], 866 + }); 867 + }); 868 + } 869 + } else { 870 + zodSchema.expression = compiler.callExpression({ 871 + functionName: compiler.propertyAccessExpression({ 872 + expression: identifiers.z, 873 + name: identifiers.union, 874 + }), 875 + parameters: [ 876 + compiler.arrayLiteralExpression({ 877 + elements: itemTypes, 878 + }), 879 + ], 880 + }); 881 + } 882 + } else { 883 + zodSchema = schemaToZodSchema({ plugin, schema, state }); 884 + } 885 + } else { 886 + // catch-all fallback for failed schemas 887 + const zSchema = schemaTypeToZodSchema({ 888 + plugin, 889 + schema: { 890 + type: 'unknown', 891 + }, 892 + state, 893 + }); 894 + zodSchema.expression = zSchema.expression; 895 + zodSchema.typeName = zSchema.anyType; 896 + } 897 + 898 + if (zodSchema.expression) { 899 + if (schema.accessScope === 'read') { 900 + zodSchema.expression = compiler.callExpression({ 901 + functionName: compiler.propertyAccessExpression({ 902 + expression: zodSchema.expression, 903 + name: identifiers.readonly, 904 + }), 905 + }); 906 + } 907 + 908 + if (optional) { 909 + zodSchema.expression = compiler.callExpression({ 910 + functionName: compiler.propertyAccessExpression({ 911 + expression: zodSchema.expression, 912 + name: identifiers.optional, 913 + }), 914 + }); 915 + } 916 + 917 + if (schema.default !== undefined) { 918 + const isBigInt = schema.type === 'integer' && schema.format === 'int64'; 919 + const callParameter = numberParameter({ 920 + isBigInt, 921 + value: schema.default, 922 + }); 923 + if (callParameter) { 924 + zodSchema.expression = compiler.callExpression({ 925 + functionName: compiler.propertyAccessExpression({ 926 + expression: zodSchema.expression, 927 + name: identifiers.default, 928 + }), 929 + parameters: [callParameter], 930 + }); 931 + } 932 + } 933 + } 934 + 935 + if (state.hasCircularReference) { 936 + if (!zodSchema.typeName) { 937 + zodSchema.typeName = 'ZodTypeAny'; 938 + } 939 + } else { 940 + zodSchema.typeName = undefined; 941 + } 942 + 943 + return zodSchema as ZodSchema; 944 + }; 945 + 946 + const handleComponent = ({ 947 + id, 948 + plugin, 949 + schema, 950 + state, 951 + }: { 952 + id: string; 953 + plugin: ZodPlugin['Instance']; 954 + schema: IR.SchemaObject; 955 + state?: State; 956 + }): void => { 957 + if (!state) { 958 + state = { 959 + circularReferenceTracker: [id], 960 + hasCircularReference: false, 961 + }; 962 + } 963 + 964 + const file = plugin.context.file({ id: zodId })!; 965 + const schemaId = plugin.api.getId({ type: 'ref', value: id }); 966 + 967 + if (file.getName(schemaId)) return; 968 + 969 + const zodSchema = schemaToZodSchema({ plugin, schema, state }); 970 + const typeInferId = plugin.config.definitions.types.infer.enabled 971 + ? plugin.api.getId({ type: 'type-infer-ref', value: id }) 972 + : undefined; 973 + exportZodSchema({ 974 + plugin, 975 + schema, 976 + schemaId, 977 + typeInferId, 978 + zodSchema, 979 + }); 980 + const baseName = refToName(id); 981 + file.updateNodeReferences( 982 + schemaId, 983 + buildName({ 984 + config: plugin.config.definitions, 985 + name: baseName, 986 + }), 987 + ); 988 + if (typeInferId) { 989 + file.updateNodeReferences( 990 + typeInferId, 991 + buildName({ 992 + config: plugin.config.definitions.types.infer, 993 + name: baseName, 994 + }), 995 + ); 996 + } 997 + }; 998 + 999 + export const handlerMini: ZodPlugin['Handler'] = ({ plugin }) => { 1000 + const file = plugin.createFile({ 1001 + case: plugin.config.case, 1002 + id: zodId, 1003 + path: plugin.output, 1004 + }); 1005 + 1006 + file.import({ 1007 + module: getZodModule({ plugin }), 1008 + name: 'z', 1009 + }); 1010 + 1011 + plugin.forEach('operation', 'parameter', 'requestBody', 'schema', (event) => { 1012 + if (event.type === 'operation') { 1013 + operationToZodSchema({ operation: event.operation, plugin }); 1014 + } else if (event.type === 'parameter') { 1015 + handleComponent({ 1016 + id: event.$ref, 1017 + plugin, 1018 + schema: event.parameter.schema, 1019 + }); 1020 + } else if (event.type === 'requestBody') { 1021 + handleComponent({ 1022 + id: event.$ref, 1023 + plugin, 1024 + schema: event.requestBody.schema, 1025 + }); 1026 + } else if (event.type === 'schema') { 1027 + handleComponent({ 1028 + id: event.$ref, 1029 + plugin, 1030 + schema: event.schema, 1031 + }); 1032 + } 1033 + }); 1034 + };
+6 -6
packages/openapi-ts/src/plugins/zod/plugin.ts
··· 1 + import { handlerMini } from './mini/plugin'; 1 2 import type { ZodPlugin } from './types'; 2 3 import { handlerV3 } from './v3/plugin'; 4 + import { handlerV4 } from './v4/plugin'; 3 5 4 6 export const handler: ZodPlugin['Handler'] = (args) => { 5 7 const { plugin } = args; 6 - 7 8 switch (plugin.config.compatibilityVersion) { 8 - case 3: 9 - return handlerV3(args); 10 9 case 4: 11 - case 'mini': 12 10 default: 13 - // TODO: handle Zod 4 14 - // TODO: handle Zod Mini 11 + return handlerV4(args); 12 + case 'mini': 13 + return handlerMini(args); 14 + case 3: 15 15 return handlerV3(args); 16 16 } 17 17 };
+33
packages/openapi-ts/src/plugins/zod/shared/module.ts
··· 1 + import type { ZodPlugin } from '../types'; 2 + 3 + export const getZodModule = ({ 4 + plugin, 5 + }: { 6 + plugin: ZodPlugin['Instance']; 7 + }): string => { 8 + const version = plugin.package.getVersion('zod'); 9 + 10 + if (version) { 11 + if (plugin.package.satisfies(version, '<4.0.0')) { 12 + switch (plugin.config.compatibilityVersion) { 13 + case 3: 14 + default: 15 + return 'zod'; 16 + case 4: 17 + return 'zod/v4'; 18 + case 'mini': 19 + return 'zod/v4-mini'; 20 + } 21 + } 22 + } 23 + 24 + switch (plugin.config.compatibilityVersion) { 25 + case 3: 26 + return 'zod/v3'; 27 + case 4: 28 + default: 29 + return 'zod'; 30 + case 'mini': 31 + return 'zod/mini'; 32 + } 33 + };
+2 -1
packages/openapi-ts/src/plugins/zod/v3/plugin.ts
··· 8 8 import { numberRegExp } from '../../../utils/regexp'; 9 9 import { identifiers, zodId } from '../constants'; 10 10 import { exportZodSchema } from '../export'; 11 + import { getZodModule } from '../shared/module'; 11 12 import type { ZodSchema } from '../shared/types'; 12 13 import type { ZodPlugin } from '../types'; 13 14 import { operationToZodSchema } from './operation'; ··· 1003 1004 }); 1004 1005 1005 1006 file.import({ 1006 - module: 'zod', 1007 + module: getZodModule({ plugin }), 1007 1008 name: 'z', 1008 1009 }); 1009 1010
+1034
packages/openapi-ts/src/plugins/zod/v4/plugin.ts
··· 1 + import ts from 'typescript'; 2 + 3 + import { compiler } from '../../../compiler'; 4 + import { deduplicateSchema } from '../../../ir/schema'; 5 + import type { IR } from '../../../ir/types'; 6 + import { buildName } from '../../../openApi/shared/utils/name'; 7 + import { refToName } from '../../../utils/ref'; 8 + import { numberRegExp } from '../../../utils/regexp'; 9 + import { identifiers, zodId } from '../constants'; 10 + import { exportZodSchema } from '../export'; 11 + import { getZodModule } from '../shared/module'; 12 + import type { ZodSchema } from '../shared/types'; 13 + import type { ZodPlugin } from '../types'; 14 + import { operationToZodSchema } from '../v3/operation'; 15 + 16 + interface SchemaWithType<T extends Required<IR.SchemaObject>['type']> 17 + extends Omit<IR.SchemaObject, 'type'> { 18 + type: Extract<Required<IR.SchemaObject>['type'], T>; 19 + } 20 + 21 + export type State = { 22 + circularReferenceTracker: Array<string>; 23 + hasCircularReference: boolean; 24 + }; 25 + 26 + const arrayTypeToZodSchema = ({ 27 + plugin, 28 + schema, 29 + state, 30 + }: { 31 + plugin: ZodPlugin['Instance']; 32 + schema: SchemaWithType<'array'>; 33 + state: State; 34 + }): ts.CallExpression => { 35 + const functionName = compiler.propertyAccessExpression({ 36 + expression: identifiers.z, 37 + name: identifiers.array, 38 + }); 39 + 40 + let arrayExpression: ts.CallExpression | undefined; 41 + 42 + if (!schema.items) { 43 + arrayExpression = compiler.callExpression({ 44 + functionName, 45 + parameters: [ 46 + unknownTypeToZodSchema({ 47 + schema: { 48 + type: 'unknown', 49 + }, 50 + }), 51 + ], 52 + }); 53 + } else { 54 + schema = deduplicateSchema({ schema }); 55 + 56 + // at least one item is guaranteed 57 + const itemExpressions = schema.items!.map( 58 + (item) => 59 + schemaToZodSchema({ 60 + plugin, 61 + schema: item, 62 + state, 63 + }).expression, 64 + ); 65 + 66 + if (itemExpressions.length === 1) { 67 + arrayExpression = compiler.callExpression({ 68 + functionName, 69 + parameters: itemExpressions, 70 + }); 71 + } else { 72 + if (schema.logicalOperator === 'and') { 73 + // TODO: parser - handle intersection 74 + // return compiler.typeArrayNode( 75 + // compiler.typeIntersectionNode({ types: itemExpressions }), 76 + // ); 77 + } 78 + 79 + arrayExpression = compiler.callExpression({ 80 + functionName: compiler.propertyAccessExpression({ 81 + expression: identifiers.z, 82 + name: identifiers.array, 83 + }), 84 + parameters: [ 85 + compiler.callExpression({ 86 + functionName: compiler.propertyAccessExpression({ 87 + expression: identifiers.z, 88 + name: identifiers.union, 89 + }), 90 + parameters: [ 91 + compiler.arrayLiteralExpression({ 92 + elements: itemExpressions, 93 + }), 94 + ], 95 + }), 96 + ], 97 + }); 98 + } 99 + } 100 + 101 + if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { 102 + arrayExpression = compiler.callExpression({ 103 + functionName: compiler.propertyAccessExpression({ 104 + expression: arrayExpression, 105 + name: identifiers.length, 106 + }), 107 + parameters: [compiler.valueToExpression({ value: schema.minItems })], 108 + }); 109 + } else { 110 + if (schema.minItems !== undefined) { 111 + arrayExpression = compiler.callExpression({ 112 + functionName: compiler.propertyAccessExpression({ 113 + expression: arrayExpression, 114 + name: identifiers.min, 115 + }), 116 + parameters: [compiler.valueToExpression({ value: schema.minItems })], 117 + }); 118 + } 119 + 120 + if (schema.maxItems !== undefined) { 121 + arrayExpression = compiler.callExpression({ 122 + functionName: compiler.propertyAccessExpression({ 123 + expression: arrayExpression, 124 + name: identifiers.max, 125 + }), 126 + parameters: [compiler.valueToExpression({ value: schema.maxItems })], 127 + }); 128 + } 129 + } 130 + 131 + return arrayExpression; 132 + }; 133 + 134 + const booleanTypeToZodSchema = ({ 135 + schema, 136 + }: { 137 + schema: SchemaWithType<'boolean'>; 138 + }) => { 139 + if (typeof schema.const === 'boolean') { 140 + const expression = compiler.callExpression({ 141 + functionName: compiler.propertyAccessExpression({ 142 + expression: identifiers.z, 143 + name: identifiers.literal, 144 + }), 145 + parameters: [compiler.ots.boolean(schema.const)], 146 + }); 147 + return expression; 148 + } 149 + 150 + const expression = compiler.callExpression({ 151 + functionName: compiler.propertyAccessExpression({ 152 + expression: identifiers.z, 153 + name: identifiers.boolean, 154 + }), 155 + }); 156 + return expression; 157 + }; 158 + 159 + const enumTypeToZodSchema = ({ 160 + schema, 161 + }: { 162 + schema: SchemaWithType<'enum'>; 163 + }): ts.CallExpression => { 164 + const enumMembers: Array<ts.LiteralExpression> = []; 165 + 166 + let isNullable = false; 167 + 168 + for (const item of schema.items ?? []) { 169 + // Zod supports only string enums 170 + if (item.type === 'string' && typeof item.const === 'string') { 171 + enumMembers.push( 172 + compiler.stringLiteral({ 173 + text: item.const, 174 + }), 175 + ); 176 + } else if (item.type === 'null' || item.const === null) { 177 + isNullable = true; 178 + } 179 + } 180 + 181 + if (!enumMembers.length) { 182 + return unknownTypeToZodSchema({ 183 + schema: { 184 + type: 'unknown', 185 + }, 186 + }); 187 + } 188 + 189 + let enumExpression = compiler.callExpression({ 190 + functionName: compiler.propertyAccessExpression({ 191 + expression: identifiers.z, 192 + name: identifiers.enum, 193 + }), 194 + parameters: [ 195 + compiler.arrayLiteralExpression({ 196 + elements: enumMembers, 197 + multiLine: false, 198 + }), 199 + ], 200 + }); 201 + 202 + if (isNullable) { 203 + enumExpression = compiler.callExpression({ 204 + functionName: compiler.propertyAccessExpression({ 205 + expression: enumExpression, 206 + name: identifiers.nullable, 207 + }), 208 + }); 209 + } 210 + 211 + return enumExpression; 212 + }; 213 + 214 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 215 + const neverTypeToZodSchema = (_props: { schema: SchemaWithType<'never'> }) => { 216 + const expression = compiler.callExpression({ 217 + functionName: compiler.propertyAccessExpression({ 218 + expression: identifiers.z, 219 + name: identifiers.never, 220 + }), 221 + }); 222 + return expression; 223 + }; 224 + 225 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 226 + const nullTypeToZodSchema = (_props: { schema: SchemaWithType<'null'> }) => { 227 + const expression = compiler.callExpression({ 228 + functionName: compiler.propertyAccessExpression({ 229 + expression: identifiers.z, 230 + name: identifiers.null, 231 + }), 232 + }); 233 + return expression; 234 + }; 235 + 236 + const numberParameter = ({ 237 + isBigInt, 238 + value, 239 + }: { 240 + isBigInt: boolean; 241 + value: unknown; 242 + }) => { 243 + const expression = compiler.valueToExpression({ value }); 244 + 245 + if ( 246 + isBigInt && 247 + (typeof value === 'bigint' || 248 + typeof value === 'number' || 249 + typeof value === 'string' || 250 + typeof value === 'boolean') 251 + ) { 252 + return compiler.callExpression({ 253 + functionName: 'BigInt', 254 + parameters: [expression], 255 + }); 256 + } 257 + 258 + return expression; 259 + }; 260 + 261 + const numberTypeToZodSchema = ({ 262 + schema, 263 + }: { 264 + schema: SchemaWithType<'integer' | 'number'>; 265 + }) => { 266 + const isBigInt = schema.type === 'integer' && schema.format === 'int64'; 267 + 268 + if (typeof schema.const === 'number') { 269 + // TODO: parser - handle bigint constants 270 + const expression = compiler.callExpression({ 271 + functionName: compiler.propertyAccessExpression({ 272 + expression: identifiers.z, 273 + name: identifiers.literal, 274 + }), 275 + parameters: [compiler.ots.number(schema.const)], 276 + }); 277 + return expression; 278 + } 279 + 280 + let numberExpression = compiler.callExpression({ 281 + functionName: isBigInt 282 + ? compiler.propertyAccessExpression({ 283 + expression: compiler.propertyAccessExpression({ 284 + expression: identifiers.z, 285 + name: identifiers.coerce, 286 + }), 287 + name: identifiers.bigint, 288 + }) 289 + : compiler.propertyAccessExpression({ 290 + expression: identifiers.z, 291 + name: identifiers.number, 292 + }), 293 + }); 294 + 295 + if (!isBigInt && schema.type === 'integer') { 296 + numberExpression = compiler.callExpression({ 297 + functionName: compiler.propertyAccessExpression({ 298 + expression: numberExpression, 299 + name: identifiers.int, 300 + }), 301 + }); 302 + } 303 + 304 + if (schema.exclusiveMinimum !== undefined) { 305 + numberExpression = compiler.callExpression({ 306 + functionName: compiler.propertyAccessExpression({ 307 + expression: numberExpression, 308 + name: identifiers.gt, 309 + }), 310 + parameters: [ 311 + numberParameter({ isBigInt, value: schema.exclusiveMinimum }), 312 + ], 313 + }); 314 + } else if (schema.minimum !== undefined) { 315 + numberExpression = compiler.callExpression({ 316 + functionName: compiler.propertyAccessExpression({ 317 + expression: numberExpression, 318 + name: identifiers.gte, 319 + }), 320 + parameters: [numberParameter({ isBigInt, value: schema.minimum })], 321 + }); 322 + } 323 + 324 + if (schema.exclusiveMaximum !== undefined) { 325 + numberExpression = compiler.callExpression({ 326 + functionName: compiler.propertyAccessExpression({ 327 + expression: numberExpression, 328 + name: identifiers.lt, 329 + }), 330 + parameters: [ 331 + numberParameter({ isBigInt, value: schema.exclusiveMaximum }), 332 + ], 333 + }); 334 + } else if (schema.maximum !== undefined) { 335 + numberExpression = compiler.callExpression({ 336 + functionName: compiler.propertyAccessExpression({ 337 + expression: numberExpression, 338 + name: identifiers.lte, 339 + }), 340 + parameters: [numberParameter({ isBigInt, value: schema.maximum })], 341 + }); 342 + } 343 + 344 + return numberExpression; 345 + }; 346 + 347 + const objectTypeToZodSchema = ({ 348 + plugin, 349 + schema, 350 + state, 351 + }: { 352 + plugin: ZodPlugin['Instance']; 353 + schema: SchemaWithType<'object'>; 354 + state: State; 355 + }): { 356 + anyType: string; 357 + expression: ts.CallExpression; 358 + } => { 359 + // TODO: parser - handle constants 360 + const properties: Array<ts.PropertyAssignment> = []; 361 + 362 + const required = schema.required ?? []; 363 + 364 + for (const name in schema.properties) { 365 + const property = schema.properties[name]!; 366 + const isRequired = required.includes(name); 367 + 368 + const propertyExpression = schemaToZodSchema({ 369 + optional: !isRequired, 370 + plugin, 371 + schema: property, 372 + state, 373 + }).expression; 374 + 375 + numberRegExp.lastIndex = 0; 376 + let propertyName; 377 + if (numberRegExp.test(name)) { 378 + // For numeric literals, we'll handle negative numbers by using a string literal 379 + // instead of trying to use a PrefixUnaryExpression 380 + propertyName = name.startsWith('-') 381 + ? ts.factory.createStringLiteral(name) 382 + : ts.factory.createNumericLiteral(name); 383 + } else { 384 + propertyName = name; 385 + } 386 + // TODO: parser - abstract safe property name logic 387 + if ( 388 + ((name.match(/^[0-9]/) && name.match(/\D+/g)) || name.match(/\W/g)) && 389 + !name.startsWith("'") && 390 + !name.endsWith("'") 391 + ) { 392 + propertyName = `'${name}'`; 393 + } 394 + properties.push( 395 + compiler.propertyAssignment({ 396 + initializer: propertyExpression, 397 + name: propertyName, 398 + }), 399 + ); 400 + } 401 + 402 + if ( 403 + schema.additionalProperties && 404 + schema.additionalProperties.type === 'object' && 405 + !Object.keys(properties).length 406 + ) { 407 + const zodSchema = schemaToZodSchema({ 408 + plugin, 409 + schema: schema.additionalProperties, 410 + state, 411 + }).expression; 412 + const expression = compiler.callExpression({ 413 + functionName: compiler.propertyAccessExpression({ 414 + expression: identifiers.z, 415 + name: identifiers.record, 416 + }), 417 + parameters: [zodSchema], 418 + }); 419 + return { 420 + anyType: 'AnyZodObject', 421 + expression, 422 + }; 423 + } 424 + 425 + const expression = compiler.callExpression({ 426 + functionName: compiler.propertyAccessExpression({ 427 + expression: identifiers.z, 428 + name: identifiers.object, 429 + }), 430 + parameters: [ts.factory.createObjectLiteralExpression(properties, true)], 431 + }); 432 + return { 433 + anyType: 'AnyZodObject', 434 + expression, 435 + }; 436 + }; 437 + 438 + const stringTypeToZodSchema = ({ 439 + plugin, 440 + schema, 441 + }: { 442 + plugin: ZodPlugin['Instance']; 443 + schema: SchemaWithType<'string'>; 444 + }) => { 445 + if (typeof schema.const === 'string') { 446 + const expression = compiler.callExpression({ 447 + functionName: compiler.propertyAccessExpression({ 448 + expression: identifiers.z, 449 + name: identifiers.literal, 450 + }), 451 + parameters: [compiler.ots.string(schema.const)], 452 + }); 453 + return expression; 454 + } 455 + 456 + let stringExpression = compiler.callExpression({ 457 + functionName: compiler.propertyAccessExpression({ 458 + expression: identifiers.z, 459 + name: identifiers.string, 460 + }), 461 + }); 462 + 463 + if (schema.format) { 464 + switch (schema.format) { 465 + case 'date-time': 466 + stringExpression = compiler.callExpression({ 467 + functionName: compiler.propertyAccessExpression({ 468 + expression: stringExpression, 469 + name: identifiers.datetime, 470 + }), 471 + parameters: plugin.config.dates.offset 472 + ? [ 473 + compiler.objectExpression({ 474 + obj: [ 475 + { 476 + key: 'offset', 477 + value: true, 478 + }, 479 + ], 480 + }), 481 + ] 482 + : [], 483 + }); 484 + break; 485 + case 'ipv4': 486 + case 'ipv6': 487 + stringExpression = compiler.callExpression({ 488 + functionName: compiler.propertyAccessExpression({ 489 + expression: stringExpression, 490 + name: identifiers.ip, 491 + }), 492 + }); 493 + break; 494 + case 'uri': 495 + stringExpression = compiler.callExpression({ 496 + functionName: compiler.propertyAccessExpression({ 497 + expression: stringExpression, 498 + name: identifiers.url, 499 + }), 500 + }); 501 + break; 502 + case 'date': 503 + case 'email': 504 + case 'time': 505 + case 'uuid': 506 + stringExpression = compiler.callExpression({ 507 + functionName: compiler.propertyAccessExpression({ 508 + expression: stringExpression, 509 + name: compiler.identifier({ text: schema.format }), 510 + }), 511 + }); 512 + break; 513 + } 514 + } 515 + 516 + if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { 517 + stringExpression = compiler.callExpression({ 518 + functionName: compiler.propertyAccessExpression({ 519 + expression: stringExpression, 520 + name: identifiers.length, 521 + }), 522 + parameters: [compiler.valueToExpression({ value: schema.minLength })], 523 + }); 524 + } else { 525 + if (schema.minLength !== undefined) { 526 + stringExpression = compiler.callExpression({ 527 + functionName: compiler.propertyAccessExpression({ 528 + expression: stringExpression, 529 + name: identifiers.min, 530 + }), 531 + parameters: [compiler.valueToExpression({ value: schema.minLength })], 532 + }); 533 + } 534 + 535 + if (schema.maxLength !== undefined) { 536 + stringExpression = compiler.callExpression({ 537 + functionName: compiler.propertyAccessExpression({ 538 + expression: stringExpression, 539 + name: identifiers.max, 540 + }), 541 + parameters: [compiler.valueToExpression({ value: schema.maxLength })], 542 + }); 543 + } 544 + } 545 + 546 + if (schema.pattern) { 547 + stringExpression = compiler.callExpression({ 548 + functionName: compiler.propertyAccessExpression({ 549 + expression: stringExpression, 550 + name: identifiers.regex, 551 + }), 552 + parameters: [compiler.regularExpressionLiteral({ text: schema.pattern })], 553 + }); 554 + } 555 + 556 + return stringExpression; 557 + }; 558 + 559 + const tupleTypeToZodSchema = ({ 560 + plugin, 561 + schema, 562 + state, 563 + }: { 564 + plugin: ZodPlugin['Instance']; 565 + schema: SchemaWithType<'tuple'>; 566 + state: State; 567 + }) => { 568 + if (schema.const && Array.isArray(schema.const)) { 569 + const tupleElements = schema.const.map((value) => 570 + compiler.callExpression({ 571 + functionName: compiler.propertyAccessExpression({ 572 + expression: identifiers.z, 573 + name: identifiers.literal, 574 + }), 575 + parameters: [compiler.valueToExpression({ value })], 576 + }), 577 + ); 578 + const expression = compiler.callExpression({ 579 + functionName: compiler.propertyAccessExpression({ 580 + expression: identifiers.z, 581 + name: identifiers.tuple, 582 + }), 583 + parameters: [ 584 + compiler.arrayLiteralExpression({ 585 + elements: tupleElements, 586 + }), 587 + ], 588 + }); 589 + return expression; 590 + } 591 + 592 + const tupleElements: Array<ts.Expression> = []; 593 + 594 + for (const item of schema.items ?? []) { 595 + tupleElements.push( 596 + schemaToZodSchema({ 597 + plugin, 598 + schema: item, 599 + state, 600 + }).expression, 601 + ); 602 + } 603 + 604 + const expression = compiler.callExpression({ 605 + functionName: compiler.propertyAccessExpression({ 606 + expression: identifiers.z, 607 + name: identifiers.tuple, 608 + }), 609 + parameters: [ 610 + compiler.arrayLiteralExpression({ 611 + elements: tupleElements, 612 + }), 613 + ], 614 + }); 615 + return expression; 616 + }; 617 + 618 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 619 + const undefinedTypeToZodSchema = (_props: { 620 + schema: SchemaWithType<'undefined'>; 621 + }) => { 622 + const expression = compiler.callExpression({ 623 + functionName: compiler.propertyAccessExpression({ 624 + expression: identifiers.z, 625 + name: identifiers.undefined, 626 + }), 627 + }); 628 + return expression; 629 + }; 630 + 631 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 632 + const unknownTypeToZodSchema = (_props: { 633 + schema: SchemaWithType<'unknown'>; 634 + }) => { 635 + const expression = compiler.callExpression({ 636 + functionName: compiler.propertyAccessExpression({ 637 + expression: identifiers.z, 638 + name: identifiers.unknown, 639 + }), 640 + }); 641 + return expression; 642 + }; 643 + 644 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 645 + const voidTypeToZodSchema = (_props: { schema: SchemaWithType<'void'> }) => { 646 + const expression = compiler.callExpression({ 647 + functionName: compiler.propertyAccessExpression({ 648 + expression: identifiers.z, 649 + name: identifiers.void, 650 + }), 651 + }); 652 + return expression; 653 + }; 654 + 655 + const schemaTypeToZodSchema = ({ 656 + plugin, 657 + schema, 658 + state, 659 + }: { 660 + plugin: ZodPlugin['Instance']; 661 + schema: IR.SchemaObject; 662 + state: State; 663 + }): { 664 + anyType?: string; 665 + expression: ts.Expression; 666 + } => { 667 + switch (schema.type as Required<IR.SchemaObject>['type']) { 668 + case 'array': 669 + return { 670 + expression: arrayTypeToZodSchema({ 671 + plugin, 672 + schema: schema as SchemaWithType<'array'>, 673 + state, 674 + }), 675 + }; 676 + case 'boolean': 677 + return { 678 + expression: booleanTypeToZodSchema({ 679 + schema: schema as SchemaWithType<'boolean'>, 680 + }), 681 + }; 682 + case 'enum': 683 + return { 684 + expression: enumTypeToZodSchema({ 685 + schema: schema as SchemaWithType<'enum'>, 686 + }), 687 + }; 688 + case 'integer': 689 + case 'number': 690 + return { 691 + expression: numberTypeToZodSchema({ 692 + schema: schema as SchemaWithType<'integer' | 'number'>, 693 + }), 694 + }; 695 + case 'never': 696 + return { 697 + expression: neverTypeToZodSchema({ 698 + schema: schema as SchemaWithType<'never'>, 699 + }), 700 + }; 701 + case 'null': 702 + return { 703 + expression: nullTypeToZodSchema({ 704 + schema: schema as SchemaWithType<'null'>, 705 + }), 706 + }; 707 + case 'object': 708 + return objectTypeToZodSchema({ 709 + plugin, 710 + schema: schema as SchemaWithType<'object'>, 711 + state, 712 + }); 713 + case 'string': 714 + return { 715 + expression: stringTypeToZodSchema({ 716 + plugin, 717 + schema: schema as SchemaWithType<'string'>, 718 + }), 719 + }; 720 + case 'tuple': 721 + return { 722 + expression: tupleTypeToZodSchema({ 723 + plugin, 724 + schema: schema as SchemaWithType<'tuple'>, 725 + state, 726 + }), 727 + }; 728 + case 'undefined': 729 + return { 730 + expression: undefinedTypeToZodSchema({ 731 + schema: schema as SchemaWithType<'undefined'>, 732 + }), 733 + }; 734 + case 'unknown': 735 + return { 736 + expression: unknownTypeToZodSchema({ 737 + schema: schema as SchemaWithType<'unknown'>, 738 + }), 739 + }; 740 + case 'void': 741 + return { 742 + expression: voidTypeToZodSchema({ 743 + schema: schema as SchemaWithType<'void'>, 744 + }), 745 + }; 746 + } 747 + }; 748 + 749 + export const schemaToZodSchema = ({ 750 + optional, 751 + plugin, 752 + schema, 753 + state, 754 + }: { 755 + /** 756 + * Accept `optional` to handle optional object properties. We can't handle 757 + * this inside the object function because `.optional()` must come before 758 + * `.default()` which is handled in this function. 759 + */ 760 + optional?: boolean; 761 + plugin: ZodPlugin['Instance']; 762 + schema: IR.SchemaObject; 763 + state: State; 764 + }): ZodSchema => { 765 + const file = plugin.context.file({ id: zodId })!; 766 + 767 + let zodSchema: Partial<ZodSchema> = {}; 768 + 769 + if (schema.$ref) { 770 + const isCircularReference = state.circularReferenceTracker.includes( 771 + schema.$ref, 772 + ); 773 + state.circularReferenceTracker.push(schema.$ref); 774 + 775 + const id = plugin.api.getId({ type: 'ref', value: schema.$ref }); 776 + 777 + if (isCircularReference) { 778 + const expression = file.addNodeReference(id, { 779 + factory: (text) => compiler.identifier({ text }), 780 + }); 781 + zodSchema.expression = compiler.callExpression({ 782 + functionName: compiler.propertyAccessExpression({ 783 + expression: identifiers.z, 784 + name: identifiers.lazy, 785 + }), 786 + parameters: [ 787 + compiler.arrowFunction({ 788 + statements: [compiler.returnStatement({ expression })], 789 + }), 790 + ], 791 + }); 792 + state.hasCircularReference = true; 793 + } else if (!file.getName(id)) { 794 + // if $ref hasn't been processed yet, inline it to avoid the 795 + // "Block-scoped variable used before its declaration." error 796 + // this could be (maybe?) fixed by reshuffling the generation order 797 + const ref = plugin.context.resolveIrRef<IR.SchemaObject>(schema.$ref); 798 + handleComponent({ 799 + id: schema.$ref, 800 + plugin, 801 + schema: ref, 802 + state, 803 + }); 804 + } 805 + 806 + if (!isCircularReference) { 807 + const expression = file.addNodeReference(id, { 808 + factory: (text) => compiler.identifier({ text }), 809 + }); 810 + zodSchema.expression = expression; 811 + } 812 + 813 + state.circularReferenceTracker.pop(); 814 + } else if (schema.type) { 815 + const zSchema = schemaTypeToZodSchema({ plugin, schema, state }); 816 + zodSchema.expression = zSchema.expression; 817 + zodSchema.typeName = zSchema.anyType; 818 + 819 + if (plugin.config.metadata && schema.description) { 820 + zodSchema.expression = compiler.callExpression({ 821 + functionName: compiler.propertyAccessExpression({ 822 + expression: zodSchema.expression, 823 + name: identifiers.describe, 824 + }), 825 + parameters: [compiler.stringLiteral({ text: schema.description })], 826 + }); 827 + } 828 + } else if (schema.items) { 829 + schema = deduplicateSchema({ schema }); 830 + 831 + if (schema.items) { 832 + const itemTypes = schema.items.map( 833 + (item) => 834 + schemaToZodSchema({ 835 + plugin, 836 + schema: item, 837 + state, 838 + }).expression, 839 + ); 840 + 841 + if (schema.logicalOperator === 'and') { 842 + const firstSchema = schema.items[0]!; 843 + // we want to add an intersection, but not every schema can use the same API. 844 + // if the first item contains another array or not an object, we cannot use 845 + // `.merge()` as that does not exist on `.union()` and non-object schemas. 846 + if ( 847 + firstSchema.logicalOperator === 'or' || 848 + (firstSchema.type && firstSchema.type !== 'object') 849 + ) { 850 + zodSchema.expression = compiler.callExpression({ 851 + functionName: compiler.propertyAccessExpression({ 852 + expression: identifiers.z, 853 + name: identifiers.intersection, 854 + }), 855 + parameters: itemTypes, 856 + }); 857 + } else { 858 + zodSchema.expression = itemTypes[0]; 859 + itemTypes.slice(1).forEach((item) => { 860 + zodSchema.expression = compiler.callExpression({ 861 + functionName: compiler.propertyAccessExpression({ 862 + expression: zodSchema.expression!, 863 + name: identifiers.and, 864 + }), 865 + parameters: [item], 866 + }); 867 + }); 868 + } 869 + } else { 870 + zodSchema.expression = compiler.callExpression({ 871 + functionName: compiler.propertyAccessExpression({ 872 + expression: identifiers.z, 873 + name: identifiers.union, 874 + }), 875 + parameters: [ 876 + compiler.arrayLiteralExpression({ 877 + elements: itemTypes, 878 + }), 879 + ], 880 + }); 881 + } 882 + } else { 883 + zodSchema = schemaToZodSchema({ plugin, schema, state }); 884 + } 885 + } else { 886 + // catch-all fallback for failed schemas 887 + const zSchema = schemaTypeToZodSchema({ 888 + plugin, 889 + schema: { 890 + type: 'unknown', 891 + }, 892 + state, 893 + }); 894 + zodSchema.expression = zSchema.expression; 895 + zodSchema.typeName = zSchema.anyType; 896 + } 897 + 898 + if (zodSchema.expression) { 899 + if (schema.accessScope === 'read') { 900 + zodSchema.expression = compiler.callExpression({ 901 + functionName: compiler.propertyAccessExpression({ 902 + expression: zodSchema.expression, 903 + name: identifiers.readonly, 904 + }), 905 + }); 906 + } 907 + 908 + if (optional) { 909 + zodSchema.expression = compiler.callExpression({ 910 + functionName: compiler.propertyAccessExpression({ 911 + expression: zodSchema.expression, 912 + name: identifiers.optional, 913 + }), 914 + }); 915 + } 916 + 917 + if (schema.default !== undefined) { 918 + const isBigInt = schema.type === 'integer' && schema.format === 'int64'; 919 + const callParameter = numberParameter({ 920 + isBigInt, 921 + value: schema.default, 922 + }); 923 + if (callParameter) { 924 + zodSchema.expression = compiler.callExpression({ 925 + functionName: compiler.propertyAccessExpression({ 926 + expression: zodSchema.expression, 927 + name: identifiers.default, 928 + }), 929 + parameters: [callParameter], 930 + }); 931 + } 932 + } 933 + } 934 + 935 + if (state.hasCircularReference) { 936 + if (!zodSchema.typeName) { 937 + zodSchema.typeName = 'ZodTypeAny'; 938 + } 939 + } else { 940 + zodSchema.typeName = undefined; 941 + } 942 + 943 + return zodSchema as ZodSchema; 944 + }; 945 + 946 + const handleComponent = ({ 947 + id, 948 + plugin, 949 + schema, 950 + state, 951 + }: { 952 + id: string; 953 + plugin: ZodPlugin['Instance']; 954 + schema: IR.SchemaObject; 955 + state?: State; 956 + }): void => { 957 + if (!state) { 958 + state = { 959 + circularReferenceTracker: [id], 960 + hasCircularReference: false, 961 + }; 962 + } 963 + 964 + const file = plugin.context.file({ id: zodId })!; 965 + const schemaId = plugin.api.getId({ type: 'ref', value: id }); 966 + 967 + if (file.getName(schemaId)) return; 968 + 969 + const zodSchema = schemaToZodSchema({ plugin, schema, state }); 970 + const typeInferId = plugin.config.definitions.types.infer.enabled 971 + ? plugin.api.getId({ type: 'type-infer-ref', value: id }) 972 + : undefined; 973 + exportZodSchema({ 974 + plugin, 975 + schema, 976 + schemaId, 977 + typeInferId, 978 + zodSchema, 979 + }); 980 + const baseName = refToName(id); 981 + file.updateNodeReferences( 982 + schemaId, 983 + buildName({ 984 + config: plugin.config.definitions, 985 + name: baseName, 986 + }), 987 + ); 988 + if (typeInferId) { 989 + file.updateNodeReferences( 990 + typeInferId, 991 + buildName({ 992 + config: plugin.config.definitions.types.infer, 993 + name: baseName, 994 + }), 995 + ); 996 + } 997 + }; 998 + 999 + export const handlerV4: ZodPlugin['Handler'] = ({ plugin }) => { 1000 + const file = plugin.createFile({ 1001 + case: plugin.config.case, 1002 + id: zodId, 1003 + path: plugin.output, 1004 + }); 1005 + 1006 + file.import({ 1007 + module: getZodModule({ plugin }), 1008 + name: 'z', 1009 + }); 1010 + 1011 + plugin.forEach('operation', 'parameter', 'requestBody', 'schema', (event) => { 1012 + if (event.type === 'operation') { 1013 + operationToZodSchema({ operation: event.operation, plugin }); 1014 + } else if (event.type === 'parameter') { 1015 + handleComponent({ 1016 + id: event.$ref, 1017 + plugin, 1018 + schema: event.parameter.schema, 1019 + }); 1020 + } else if (event.type === 'requestBody') { 1021 + handleComponent({ 1022 + id: event.$ref, 1023 + plugin, 1024 + schema: event.requestBody.schema, 1025 + }); 1026 + } else if (event.type === 'schema') { 1027 + handleComponent({ 1028 + id: event.$ref, 1029 + plugin, 1030 + schema: event.schema, 1031 + }); 1032 + } 1033 + }); 1034 + };
+9 -9
pnpm-lock.yaml
··· 329 329 specifier: 4.5.0 330 330 version: 4.5.0(vue@3.5.13(typescript@5.8.3)) 331 331 zod: 332 - specifier: 3.25.0 333 - version: 3.25.0 332 + specifier: 3.25.3 333 + version: 3.25.3 334 334 devDependencies: 335 335 vite: 336 336 specifier: 6.2.7 ··· 357 357 specifier: 1.1.0 358 358 version: 1.1.0(typescript@5.8.3) 359 359 zod: 360 - specifier: 3.25.0 361 - version: 3.25.0 360 + specifier: 3.25.3 361 + version: 3.25.3 362 362 devDependencies: 363 363 '@config/vite-base': 364 364 specifier: workspace:* ··· 980 980 specifier: 3.5.13 981 981 version: 3.5.13(typescript@5.8.3) 982 982 zod: 983 - specifier: 3.25.0 984 - version: 3.25.0 983 + specifier: 3.25.3 984 + version: 3.25.3 985 985 986 986 packages/vite-plugin: 987 987 devDependencies: ··· 12051 12051 zod@3.23.8: 12052 12052 resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} 12053 12053 12054 - zod@3.25.0: 12055 - resolution: {integrity: sha512-ficnZKUW0mlNivqeJkosTEkGbJ6NKCtSaOHGx5aXbtfeWMdRyzXLbAIn19my4C/KB7WPY/p9vlGPt+qpOp6c4Q==} 12054 + zod@3.25.3: 12055 + resolution: {integrity: sha512-VGZqnyYNrl8JpEJRZaFPqeVNIuqgXNu4cXZ5cOb6zEUO1OxKbRnWB4UdDIXMmiERWncs0yDQukssHov8JUxykQ==} 12056 12056 12057 12057 zone.js@0.15.0: 12058 12058 resolution: {integrity: sha512-9oxn0IIjbCZkJ67L+LkhYWRyAy7axphb3VgE2MBDlOqnmHMPWGYMxJxBYFueFq/JGY2GMwS0rU+UCLunEmy5UA==} ··· 25016 25016 25017 25017 zod@3.23.8: {} 25018 25018 25019 - zod@3.25.0: {} 25019 + zod@3.25.3: {} 25020 25020 25021 25021 zone.js@0.15.0: {} 25022 25022