Skip to content

Commit 78a2fef

Browse files
committed
fix(shared): merge $defs instead of replacing in materializeDynamicRefBinding
When materializing a dynamic ref binding, the caller's $defs now merge with the template's $defs instead of wholesale replacing them. This preserves helper sub-schemas that the template may reference from its body alongside the anchor placeholders.
1 parent 6a7334d commit 78a2fef

5 files changed

Lines changed: 70 additions & 36 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
// This file is auto-generated by @hey-api/openapi-ts
22

3-
export type { BoundTreeNode, ClientOptions, GetTreeData, GetTreeResponse, GetTreeResponses, TreeNodeTemplate, WrapperNode } from './types.gen';
3+
export type { ClientOptions, GetTreeData, GetTreeResponse, GetTreeResponses, TreeNode, TreeNodeLeaf, TreeNodeTemplate } from './types.gen';

packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/dynamicref-circular-oneof/types.gen.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,18 @@ export type ClientOptions = {
77
export type TreeNodeTemplate<NodeType> = {
88
id: string;
99
label: string;
10-
children: Array<NodeType>;
10+
child?: NodeType;
1111
};
1212

13-
export type WrapperNode = BoundTreeNode | {
14-
leaf?: boolean;
13+
export type TreeNode = TreeNodeLeaf | {
14+
id: string;
15+
label: string;
16+
child?: TreeNode;
1517
};
1618

17-
export type BoundTreeNode = TreeNodeTemplate<WrapperNode> & {
18-
depth?: number;
19+
export type TreeNodeLeaf = {
20+
id: string;
21+
label: string;
1922
};
2023

2124
export type GetTreeData = {
@@ -27,9 +30,9 @@ export type GetTreeData = {
2730

2831
export type GetTreeResponses = {
2932
/**
30-
* Tree nodes with recursive structure through oneOf
33+
* Tree nodes
3134
*/
32-
200: Array<BoundTreeNode>;
35+
200: Array<TreeNode>;
3336
};
3437

3538
export type GetTreeResponse = GetTreeResponses[keyof GetTreeResponses];

packages/shared/src/openApi/3.1.x/parser/__tests__/dynamicRef.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,38 @@ describe('materializeDynamicRefBinding', () => {
430430

431431
expect(result!.description).toBe('overridden');
432432
});
433+
434+
it('merges $defs from refSchema and caller schema', () => {
435+
mockResolveRef.mockReturnValue({
436+
$defs: {
437+
helper: { type: 'string' },
438+
placeholder: { $dynamicAnchor: 'itemType', not: {} },
439+
},
440+
type: 'object',
441+
});
442+
443+
const result = materializeDynamicRefBinding({
444+
context: createContext(),
445+
schema: {
446+
$defs: {
447+
itemType: {
448+
$dynamicAnchor: 'itemType',
449+
$ref: '#/components/schemas/User',
450+
},
451+
},
452+
$ref: '#/components/schemas/Template',
453+
},
454+
});
455+
456+
expect(result!.$defs).toEqual({
457+
helper: { type: 'string' },
458+
itemType: {
459+
$dynamicAnchor: 'itemType',
460+
$ref: '#/components/schemas/User',
461+
},
462+
placeholder: { $dynamicAnchor: 'itemType', not: {} },
463+
});
464+
});
433465
});
434466

435467
describe('shouldInlineDynamicRefTarget', () => {

packages/shared/src/openApi/3.1.x/parser/dynamicRef.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ export function materializeDynamicRefBinding({
191191
...refSchema,
192192
...schema,
193193
};
194+
if (refSchema.$defs && schema.$defs) {
195+
materializedSchema.$defs = {
196+
...refSchema.$defs,
197+
...schema.$defs,
198+
};
199+
}
194200
delete (materializedSchema as Record<string, unknown>).$ref;
195201
delete (materializedSchema as Record<string, unknown>).$dynamicAnchor;
196202
delete (materializedSchema as Record<string, unknown>).$id;
@@ -224,10 +230,10 @@ export function containsRefTo(
224230
const composites: Array<unknown> | undefined = schema.allOf ?? schema.anyOf ?? schema.oneOf;
225231
if (Array.isArray(composites)) {
226232
for (const item of composites) {
227-
if (typeof item === 'object' && item !== null) {
233+
if (isSchemaObject(item)) {
228234
if (item.$ref === ref) return true;
229235
if (item.allOf || item.anyOf || item.oneOf) {
230-
if (containsRefTo(item as OpenAPIV3_1.SchemaObject, ref)) return true;
236+
if (containsRefTo(item, ref)) return true;
231237
}
232238
}
233239
}

specs/3.1.x/dynamicref-circular-oneof.yaml

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ openapi: 3.1.0
22
info:
33
title: DynamicRef Circular oneOf Test
44
description: >
5-
Tests that hasCircularBinding detects cycles routed through oneOf/anyOf,
6-
not just direct $ref and single-level allOf. The WrapperNode schema
7-
references back to the template through oneOf, which the narrow
8-
hasCircularBinding check would miss.
5+
Tests that hasCircularBinding detects cycles routed through oneOf,
6+
not just direct $ref and single-level allOf.
97
version: 0.1.0
108
servers:
119
- url: https://api.example.com
@@ -19,13 +17,13 @@ paths:
1917
tags: [Tree]
2018
responses:
2119
'200':
22-
description: Tree nodes with recursive structure through oneOf
20+
description: Tree nodes
2321
content:
2422
application/json:
2523
schema:
2624
type: array
2725
items:
28-
$ref: '#/components/schemas/bound-tree-node'
26+
$ref: '#/components/schemas/TreeNode'
2927

3028
components:
3129
schemas:
@@ -36,34 +34,29 @@ components:
3634
$dynamicAnchor: nodeType
3735
not: {}
3836
type: object
39-
required: [id, label, children]
37+
required: [id, label]
4038
properties:
4139
id:
4240
type: string
4341
label:
4442
type: string
45-
children:
46-
type: array
47-
items:
48-
$dynamicRef: '#nodeType'
43+
child:
44+
$dynamicRef: '#nodeType'
4945

50-
WrapperNode:
51-
type: object
46+
TreeNode:
5247
oneOf:
53-
- $ref: '#/components/schemas/bound-tree-node'
54-
- type: object
55-
properties:
56-
leaf:
57-
type: boolean
58-
59-
bound-tree-node:
60-
allOf:
48+
- $ref: '#/components/schemas/TreeNodeLeaf'
6149
- $defs:
6250
nodeType:
6351
$dynamicAnchor: nodeType
64-
$ref: '#/components/schemas/WrapperNode'
52+
$ref: '#/components/schemas/TreeNode'
6553
$ref: '#/components/schemas/TreeNodeTemplate'
66-
- type: object
67-
properties:
68-
depth:
69-
type: integer
54+
55+
TreeNodeLeaf:
56+
type: object
57+
required: [id, label]
58+
properties:
59+
id:
60+
type: string
61+
label:
62+
type: string

0 commit comments

Comments
 (0)