Skip to content

Commit 6c3b3e9

Browse files
committed
Fix mapped discriminator to self leading to $type: string instead of string literal
1 parent 66642e6 commit 6c3b3e9

10 files changed

Lines changed: 344 additions & 0 deletions

File tree

packages/openapi-ts-tests/main/test/3.0.x.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,13 @@ describe(`OpenAPI ${version}`, () => {
224224
description:
225225
'handles allOf where inline schema discriminator mapping should take priority over $ref discriminator fallback',
226226
},
227+
{
228+
config: createConfig({
229+
input: 'discriminator-object-self-mapped.json',
230+
output: 'discriminator-object-self-mapped',
231+
}),
232+
description: 'handles object discriminator mappings that include the schema itself',
233+
},
227234
{
228235
config: createConfig({
229236
input: 'discriminator-non-string.yaml',

packages/openapi-ts-tests/main/test/3.1.x.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,13 @@ describe(`OpenAPI ${version}`, () => {
250250
description:
251251
'handles allOf where inline schema discriminator mapping should take priority over $ref discriminator fallback',
252252
},
253+
{
254+
config: createConfig({
255+
input: 'discriminator-object-self-mapped.json',
256+
output: 'discriminator-object-self-mapped',
257+
}),
258+
description: 'handles object discriminator mappings that include the schema itself',
259+
},
253260
{
254261
config: createConfig({
255262
input: 'discriminator-non-string.yaml',
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// This file is auto-generated by @hey-api/openapi-ts
2+
3+
export type { BlogPostDto, BlogPostWithImageDto, ClientOptions, GetBlogPostsData, GetBlogPostsResponse, GetBlogPostsResponses } from './types.gen';
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 BlogPostDto = {
8+
$type: 'BlogPost';
9+
id: number;
10+
title: string;
11+
};
12+
13+
export type BlogPostWithImageDto = Omit<BlogPostDto, '$type'> & {
14+
imageUrl: string;
15+
$type: 'BlogPostWithImage';
16+
};
17+
18+
export type GetBlogPostsData = {
19+
body?: never;
20+
path?: never;
21+
query?: never;
22+
url: '/blog-posts';
23+
};
24+
25+
export type GetBlogPostsResponses = {
26+
/**
27+
* List of blog posts
28+
*/
29+
200: Array<BlogPostDto | BlogPostWithImageDto>;
30+
};
31+
32+
export type GetBlogPostsResponse = GetBlogPostsResponses[keyof GetBlogPostsResponses];
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// This file is auto-generated by @hey-api/openapi-ts
2+
3+
export type { BlogPostDto, BlogPostWithImageDto, ClientOptions, GetBlogPostsData, GetBlogPostsResponse, GetBlogPostsResponses } from './types.gen';
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 BlogPostDto = {
8+
$type: 'BlogPost';
9+
id: number;
10+
title: string;
11+
};
12+
13+
export type BlogPostWithImageDto = Omit<BlogPostDto, '$type'> & {
14+
imageUrl: string;
15+
$type: 'BlogPostWithImage';
16+
};
17+
18+
export type GetBlogPostsData = {
19+
body?: never;
20+
path?: never;
21+
query?: never;
22+
url: '/blog-posts';
23+
};
24+
25+
export type GetBlogPostsResponses = {
26+
/**
27+
* List of blog posts
28+
*/
29+
200: Array<BlogPostDto | BlogPostWithImageDto>;
30+
};
31+
32+
export type GetBlogPostsResponse = GetBlogPostsResponses[keyof GetBlogPostsResponses];

packages/shared/src/openApi/3.0.x/parser/schema.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,46 @@ const getAllDiscriminatorValues = ({
150150
return values;
151151
};
152152

153+
const getMappedDiscriminatorProperty = ({
154+
context,
155+
discriminator,
156+
schema,
157+
schemaRef,
158+
}: {
159+
context: Context;
160+
discriminator: NonNullable<SchemaObject['discriminator']>;
161+
schema: SchemaObject;
162+
schemaRef: string;
163+
}): IR.SchemaObject | undefined => {
164+
const values = getAllDiscriminatorValues({
165+
discriminator,
166+
schemaRef,
167+
});
168+
169+
if (!values.length) {
170+
return;
171+
}
172+
173+
const propertyType = findDiscriminatorPropertyType({
174+
context,
175+
propertyName: discriminator.propertyName,
176+
schemas: [schema],
177+
});
178+
179+
const valueSchemas: ReadonlyArray<IR.SchemaObject> = values.map((value) =>
180+
convertDiscriminatorValue(value, propertyType),
181+
);
182+
183+
if (valueSchemas.length === 1) {
184+
return valueSchemas[0];
185+
}
186+
187+
return {
188+
items: valueSchemas,
189+
logicalOperator: 'or',
190+
};
191+
};
192+
153193
const parseSchemaJsDoc = ({
154194
irSchema,
155195
schema,
@@ -377,6 +417,23 @@ const parseObject = ({
377417
irSchema.required = schema.required;
378418
}
379419

420+
if (schema.discriminator && state.$ref) {
421+
const discriminatorProperty = getMappedDiscriminatorProperty({
422+
context,
423+
discriminator: schema.discriminator,
424+
schema,
425+
schemaRef: state.$ref,
426+
});
427+
428+
if (discriminatorProperty) {
429+
if (!irSchema.properties) {
430+
irSchema.properties = {};
431+
}
432+
433+
irSchema.properties[schema.discriminator.propertyName] = discriminatorProperty;
434+
}
435+
}
436+
380437
return irSchema;
381438
};
382439

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,46 @@ const getAllDiscriminatorValues = ({
163163
return values;
164164
};
165165

166+
const getMappedDiscriminatorProperty = ({
167+
context,
168+
discriminator,
169+
schema,
170+
schemaRef,
171+
}: {
172+
context: Context;
173+
discriminator: NonNullable<SchemaObject['discriminator']>;
174+
schema: SchemaObject;
175+
schemaRef: string;
176+
}): IR.SchemaObject | undefined => {
177+
const values = getAllDiscriminatorValues({
178+
discriminator,
179+
schemaRef,
180+
});
181+
182+
if (!values.length) {
183+
return;
184+
}
185+
186+
const propertyType = findDiscriminatorPropertyType({
187+
context,
188+
propertyName: discriminator.propertyName,
189+
schemas: [schema],
190+
});
191+
192+
const valueSchemas: ReadonlyArray<IR.SchemaObject> = values.map((value) =>
193+
convertDiscriminatorValue(value, propertyType),
194+
);
195+
196+
if (valueSchemas.length === 1) {
197+
return valueSchemas[0];
198+
}
199+
200+
return {
201+
items: valueSchemas,
202+
logicalOperator: 'or',
203+
};
204+
};
205+
166206
const parseSchemaJsDoc = ({
167207
irSchema,
168208
schema,
@@ -472,6 +512,23 @@ const parseObject = ({
472512
irSchema.required = schema.required;
473513
}
474514

515+
if (schema.discriminator && state.$ref) {
516+
const discriminatorProperty = getMappedDiscriminatorProperty({
517+
context,
518+
discriminator: schema.discriminator,
519+
schema,
520+
schemaRef: state.$ref,
521+
});
522+
523+
if (discriminatorProperty) {
524+
if (!irSchema.properties) {
525+
irSchema.properties = {};
526+
}
527+
528+
irSchema.properties[schema.discriminator.propertyName] = discriminatorProperty;
529+
}
530+
}
531+
475532
return irSchema;
476533
};
477534

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
{
2+
"openapi": "3.0.3",
3+
"info": {
4+
"title": "Discriminator object schema with self mapping",
5+
"version": "1.0.0",
6+
"description": "Ensures a concrete schema with a discriminator mapping to itself gets a literal discriminator value instead of falling back to string."
7+
},
8+
"paths": {
9+
"/blog-posts": {
10+
"get": {
11+
"summary": "Get blog posts",
12+
"responses": {
13+
"200": {
14+
"description": "List of blog posts",
15+
"content": {
16+
"application/json": {
17+
"schema": {
18+
"type": "array",
19+
"items": {
20+
"oneOf": [
21+
{ "$ref": "#/components/schemas/BlogPostDto" },
22+
{ "$ref": "#/components/schemas/BlogPostWithImageDto" }
23+
]
24+
}
25+
}
26+
}
27+
}
28+
}
29+
}
30+
}
31+
}
32+
},
33+
"components": {
34+
"schemas": {
35+
"BlogPostDto": {
36+
"type": "object",
37+
"required": ["$type", "id", "title"],
38+
"properties": {
39+
"$type": {
40+
"type": "string"
41+
},
42+
"id": {
43+
"type": "integer"
44+
},
45+
"title": {
46+
"type": "string"
47+
}
48+
},
49+
"discriminator": {
50+
"propertyName": "$type",
51+
"mapping": {
52+
"BlogPost": "#/components/schemas/BlogPostDto",
53+
"BlogPostWithImage": "#/components/schemas/BlogPostWithImageDto"
54+
}
55+
}
56+
},
57+
"BlogPostWithImageDto": {
58+
"allOf": [
59+
{ "$ref": "#/components/schemas/BlogPostDto" },
60+
{
61+
"type": "object",
62+
"required": ["imageUrl"],
63+
"properties": {
64+
"imageUrl": {
65+
"type": "string"
66+
}
67+
}
68+
}
69+
]
70+
}
71+
}
72+
}
73+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
{
2+
"openapi": "3.1.0",
3+
"info": {
4+
"title": "Discriminator object schema with self mapping",
5+
"version": "1.0.0",
6+
"description": "Ensures a concrete schema with a discriminator mapping to itself gets a literal discriminator value instead of falling back to string."
7+
},
8+
"paths": {
9+
"/blog-posts": {
10+
"get": {
11+
"summary": "Get blog posts",
12+
"responses": {
13+
"200": {
14+
"description": "List of blog posts",
15+
"content": {
16+
"application/json": {
17+
"schema": {
18+
"type": "array",
19+
"items": {
20+
"oneOf": [
21+
{ "$ref": "#/components/schemas/BlogPostDto" },
22+
{ "$ref": "#/components/schemas/BlogPostWithImageDto" }
23+
]
24+
}
25+
}
26+
}
27+
}
28+
}
29+
}
30+
}
31+
}
32+
},
33+
"components": {
34+
"schemas": {
35+
"BlogPostDto": {
36+
"type": "object",
37+
"required": ["$type", "id", "title"],
38+
"properties": {
39+
"$type": {
40+
"type": "string"
41+
},
42+
"id": {
43+
"type": "integer"
44+
},
45+
"title": {
46+
"type": "string"
47+
}
48+
},
49+
"discriminator": {
50+
"propertyName": "$type",
51+
"mapping": {
52+
"BlogPost": "#/components/schemas/BlogPostDto",
53+
"BlogPostWithImage": "#/components/schemas/BlogPostWithImageDto"
54+
}
55+
}
56+
},
57+
"BlogPostWithImageDto": {
58+
"allOf": [
59+
{ "$ref": "#/components/schemas/BlogPostDto" },
60+
{
61+
"type": "object",
62+
"required": ["imageUrl"],
63+
"properties": {
64+
"imageUrl": {
65+
"type": "string"
66+
}
67+
}
68+
}
69+
]
70+
}
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)