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.

Merge pull request #2793 from hey-api/copilot/fix-request-types-schema

Fix writeOnly schema properties missing from request types in nested schemas

authored by

Lubos and committed by
GitHub
dfe04188 bd18df95

+220 -3
+5
.changeset/nervous-eyes-pay.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + --- 4 + 5 + fix(parser): writeOnly schema properties missing from request types in nested schemas
+16
packages/openapi-ts-tests/main/test/3.1.x.test.ts
··· 649 649 }, 650 650 { 651 651 config: createConfig({ 652 + input: 'transforms-read-write-nested.yaml', 653 + output: 'transforms-read-write-nested', 654 + plugins: ['@hey-api/typescript'], 655 + }), 656 + description: 'handles write-only types in nested schemas', 657 + }, 658 + { 659 + config: createConfig({ 660 + input: 'transforms-read-write-response.yaml', 661 + output: 'transforms-read-write-response', 662 + plugins: ['@hey-api/typescript'], 663 + }), 664 + description: 'handles read-only types in nested response schemas', 665 + }, 666 + { 667 + config: createConfig({ 652 668 input: 'ref-type.json', 653 669 output: 'ref-type', 654 670 }),
+3
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transforms-read-write-nested/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type * from './types.gen';
+35
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transforms-read-write-nested/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type ClientOptions = { 4 + baseUrl: `${string}://${string}` | (string & {}); 5 + }; 6 + 7 + export type CreateItemRequest = { 8 + payload: PayloadWritable; 9 + }; 10 + 11 + export type Payload = { 12 + kind: 'jpeg'; 13 + }; 14 + 15 + export type PayloadWritable = { 16 + kind: 'jpeg'; 17 + /** 18 + * Data required on write 19 + */ 20 + encoded: string; 21 + }; 22 + 23 + export type ItemCreateData = { 24 + body: CreateItemRequest; 25 + path?: never; 26 + query?: never; 27 + url: '/items'; 28 + }; 29 + 30 + export type ItemCreateResponses = { 31 + /** 32 + * Created 33 + */ 34 + 201: unknown; 35 + };
+3
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transforms-read-write-response/index.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type * from './types.gen';
+41
packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transforms-read-write-response/types.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + export type ClientOptions = { 4 + baseUrl: `${string}://${string}` | (string & {}); 5 + }; 6 + 7 + export type ItemListResponse = { 8 + items: Array<Item>; 9 + }; 10 + 11 + export type Item = { 12 + /** 13 + * Server-generated ID 14 + */ 15 + readonly id: string; 16 + name: string; 17 + /** 18 + * Server-generated timestamp 19 + */ 20 + readonly created_at?: string; 21 + }; 22 + 23 + export type ItemWritable = { 24 + name: string; 25 + }; 26 + 27 + export type ItemListData = { 28 + body?: never; 29 + path?: never; 30 + query?: never; 31 + url: '/items'; 32 + }; 33 + 34 + export type ItemListResponses = { 35 + /** 36 + * Success 37 + */ 38 + 200: ItemListResponse; 39 + }; 40 + 41 + export type ItemListResponse2 = ItemListResponses[keyof ItemListResponses];
+35 -3
packages/openapi-ts/src/openApi/shared/transforms/readWrite.ts
··· 464 464 inSchema: boolean; 465 465 node: unknown; 466 466 path: ReadonlyArray<string | number>; 467 + visited?: Set<string>; 467 468 }; 468 469 469 470 /** ··· 474 475 * @param split - The split mapping (from splitSchemas) 475 476 */ 476 477 export const updateRefsInSpec = ({ 478 + graph, 477 479 logger, 478 480 spec, 479 481 split, 480 482 }: { 483 + graph: Graph; 481 484 logger: Logger; 482 485 spec: unknown; 483 486 split: Omit<SplitSchemas, 'schemas'>; ··· 491 494 inSchema, 492 495 node, 493 496 path, 497 + visited = new Set(), 494 498 }: WalkArgs): void => { 495 499 if (node instanceof Array) { 496 500 node.forEach((item, index) => ··· 500 504 inSchema, 501 505 node: item, 502 506 path: [...path, index], 507 + visited, 503 508 }), 504 509 ); 505 510 } else if (node && typeof node === 'object') { ··· 519 524 } else if (mapping?.write === nextPointer) { 520 525 nextContext = 'write'; 521 526 } 527 + } else { 528 + // Not a split variant - check graph for the schema's scopes 529 + const nodeInfo = graph.nodes.get(nextPointer); 530 + if (nodeInfo?.scopes) { 531 + // If schema has only write scope, use write context 532 + // If schema has only read scope, use read context 533 + // If schema has both or neither, leave context as-is (null) 534 + const hasRead = nodeInfo.scopes.has('read'); 535 + const hasWrite = nodeInfo.scopes.has('write'); 536 + if (hasWrite && !hasRead) { 537 + nextContext = 'write'; 538 + } else if (hasRead && !hasWrite) { 539 + nextContext = 'read'; 540 + } 541 + } 522 542 } 523 543 } 524 544 ··· 535 555 inSchema: false, 536 556 node: (node as Record<string, unknown>)[key], 537 557 path: [...path, key], 558 + visited, 538 559 }); 539 560 } 540 561 return; ··· 555 576 inSchema: false, 556 577 node: value, 557 578 path: [...path, key], 579 + visited, 558 580 }); 559 581 continue; 560 582 } ··· 565 587 inSchema: false, 566 588 node: value, 567 589 path: [...path, key], 590 + visited, 568 591 }); 569 592 continue; 570 593 } ··· 577 600 inSchema: true, 578 601 node: param.schema, 579 602 path: [...path, key, index, 'schema'], 603 + visited, 580 604 }); 581 605 } 582 606 // Also handle content (OpenAPI 3.x) ··· 587 611 inSchema: false, 588 612 node: param.content, 589 613 path: [...path, key, index, 'content'], 614 + visited, 590 615 }); 591 616 } 592 617 }); ··· 608 633 inSchema: false, 609 634 node: (value as Record<string, unknown>)[headerKey], 610 635 path: [...path, key, headerKey], 636 + visited, 611 637 }); 612 638 } 613 639 continue; ··· 622 648 inSchema: true, 623 649 node: value, 624 650 path: [...path, key], 651 + visited, 625 652 }); 626 653 } else if (key === '$ref' && typeof value === 'string') { 627 654 // Prefer exact match first 628 655 const map = split.mapping[value]; 629 656 if (map) { 630 - if (map.read && (!nextContext || nextContext === 'read')) { 657 + if (nextContext === 'read' && map.read) { 631 658 (node as Record<string, unknown>)[key] = map.read; 632 - } else if (map.write && (!nextContext || nextContext === 'write')) { 659 + } else if (nextContext === 'write' && map.write) { 633 660 (node as Record<string, unknown>)[key] = map.write; 661 + } else if (!nextContext && map.read) { 662 + // For schemas with no context (unused in operations), default to read variant 663 + // This ensures $refs in unused schemas don't point to removed originals 664 + (node as Record<string, unknown>)[key] = map.read; 634 665 } 635 666 } 636 667 } else { ··· 640 671 inSchema, 641 672 node: value, 642 673 path: [...path, key], 674 + visited, 643 675 }); 644 676 } 645 677 } ··· 679 711 const originalSchemas = captureOriginalSchemas(spec, logger); 680 712 const split = splitSchemas({ config, graph, logger, spec }); 681 713 insertSplitSchemasIntoSpec({ logger, spec, split }); 682 - updateRefsInSpec({ logger, spec, split }); 714 + updateRefsInSpec({ graph, logger, spec, split }); 683 715 removeOriginalSplitSchemas({ logger, originalSchemas, spec, split }); 684 716 };
+39
specs/3.1.x/transforms-read-write-nested.yaml
··· 1 + openapi: 3.0.3 2 + info: 3 + title: writeOnly repro 4 + version: 1.0.0 5 + paths: 6 + /items: 7 + post: 8 + operationId: item_create 9 + requestBody: 10 + required: true 11 + content: 12 + application/json: 13 + schema: 14 + $ref: '#/components/schemas/CreateItemRequest' 15 + responses: 16 + '201': 17 + description: Created 18 + components: 19 + schemas: 20 + CreateItemRequest: 21 + type: object 22 + required: 23 + - payload 24 + properties: 25 + payload: 26 + $ref: '#/components/schemas/Payload' 27 + Payload: 28 + type: object 29 + required: 30 + - kind 31 + - encoded 32 + properties: 33 + kind: 34 + type: string 35 + enum: [jpeg] 36 + encoded: 37 + type: string 38 + writeOnly: true 39 + description: Data required on write
+43
specs/3.1.x/transforms-read-write-response.yaml
··· 1 + openapi: 3.0.3 2 + info: 3 + title: readOnly response test 4 + version: 1.0.0 5 + paths: 6 + /items: 7 + get: 8 + operationId: item_list 9 + responses: 10 + '200': 11 + description: Success 12 + content: 13 + application/json: 14 + schema: 15 + $ref: '#/components/schemas/ItemListResponse' 16 + components: 17 + schemas: 18 + ItemListResponse: 19 + type: object 20 + required: 21 + - items 22 + properties: 23 + items: 24 + type: array 25 + items: 26 + $ref: '#/components/schemas/Item' 27 + Item: 28 + type: object 29 + required: 30 + - id 31 + - name 32 + properties: 33 + id: 34 + type: string 35 + readOnly: true 36 + description: Server-generated ID 37 + name: 38 + type: string 39 + created_at: 40 + type: string 41 + format: date-time 42 + readOnly: true 43 + description: Server-generated timestamp