Skip to content

Commit 26744e1

Browse files
authored
Merge pull request #3385 from SipanP/fix/discriminator-non-string-type-support
fix(shared): support non-string discriminator property types
2 parents bb53a98 + a41b883 commit 26744e1

12 files changed

Lines changed: 792 additions & 26 deletions

File tree

.changeset/wicked-rings-march.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hey-api/shared": patch
3+
---
4+
5+
Support non-string discriminator property types (boolean, integer, number)

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
@@ -217,6 +217,13 @@ describe(`OpenAPI ${version}`, () => {
217217
}),
218218
description: 'handles nested allOf with discriminators',
219219
},
220+
{
221+
config: createConfig({
222+
input: 'discriminator-non-string.yaml',
223+
output: 'discriminator-non-string',
224+
}),
225+
description: 'handles non-string discriminator property types',
226+
},
220227
{
221228
config: createConfig({
222229
input: 'enum-escape.json',

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
@@ -236,6 +236,13 @@ describe(`OpenAPI ${version}`, () => {
236236
}),
237237
description: 'handles nested allOf with discriminators',
238238
},
239+
{
240+
config: createConfig({
241+
input: 'discriminator-non-string.yaml',
242+
output: 'discriminator-non-string',
243+
}),
244+
description: 'handles non-string discriminator property types',
245+
},
239246
{
240247
config: createConfig({
241248
input: 'discriminator-one-of-read-write.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 { AutoConfig, BooleanAnyOf, BooleanOneOf, ClientOptions, CustomConfig, IntegerAllOfBase, IntegerAllOfChildA, IntegerAllOfChildB, IntegerOneOf, NumberOneOf, TypeOne, TypeTwo, VersionAlpha, VersionBeta } from './types.gen';
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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 BooleanOneOf = ({
8+
use_custom: false;
9+
} & AutoConfig) | ({
10+
use_custom: true;
11+
} & CustomConfig);
12+
13+
export type AutoConfig = {
14+
use_custom: boolean;
15+
auto_setting: string;
16+
};
17+
18+
export type CustomConfig = {
19+
use_custom: boolean;
20+
custom_value: number;
21+
};
22+
23+
export type BooleanAnyOf = ({
24+
use_custom?: false;
25+
} & AutoConfig) | ({
26+
use_custom?: true;
27+
} & CustomConfig);
28+
29+
export type IntegerOneOf = ({
30+
type_id: 1;
31+
} & TypeOne) | ({
32+
type_id: 2;
33+
} & TypeTwo);
34+
35+
export type TypeOne = {
36+
type_id: number;
37+
one_data: string;
38+
};
39+
40+
export type TypeTwo = {
41+
type_id: number;
42+
two_data: string;
43+
};
44+
45+
export type NumberOneOf = ({
46+
version: 1;
47+
} & VersionAlpha) | ({
48+
version: 2.5;
49+
} & VersionBeta);
50+
51+
export type VersionAlpha = {
52+
version: number;
53+
alpha_field: string;
54+
};
55+
56+
export type VersionBeta = {
57+
version: number;
58+
beta_field: string;
59+
};
60+
61+
export type IntegerAllOfBase = {
62+
kind: number;
63+
};
64+
65+
export type IntegerAllOfChildA = Omit<IntegerAllOfBase, 'kind'> & {
66+
child_a_field: string;
67+
kind: 1;
68+
};
69+
70+
export type IntegerAllOfChildB = Omit<IntegerAllOfBase, 'kind'> & {
71+
child_b_field: string;
72+
kind: 2;
73+
};
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 { AutoConfig, BooleanAnyOf, BooleanOneOf, ClientOptions, CustomConfig, IntegerAllOfBase, IntegerAllOfChildA, IntegerAllOfChildB, IntegerOneOf, NullableIntegerOneOf, NullableVariantX, NullableVariantY, NumberOneOf, TypeOne, TypeTwo, VersionAlpha, VersionBeta } from './types.gen';
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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 BooleanOneOf = ({
8+
use_custom: false;
9+
} & AutoConfig) | ({
10+
use_custom: true;
11+
} & CustomConfig);
12+
13+
export type AutoConfig = {
14+
use_custom: false;
15+
auto_setting: string;
16+
};
17+
18+
export type CustomConfig = {
19+
use_custom: true;
20+
custom_value: number;
21+
};
22+
23+
export type BooleanAnyOf = ({
24+
use_custom?: false;
25+
} & AutoConfig) | ({
26+
use_custom?: true;
27+
} & CustomConfig);
28+
29+
export type IntegerOneOf = ({
30+
type_id: 1;
31+
} & TypeOne) | ({
32+
type_id: 2;
33+
} & TypeTwo);
34+
35+
export type TypeOne = {
36+
type_id: 1;
37+
one_data: string;
38+
};
39+
40+
export type TypeTwo = {
41+
type_id: 2;
42+
two_data: string;
43+
};
44+
45+
export type NumberOneOf = ({
46+
version: 1;
47+
} & VersionAlpha) | ({
48+
version: 2.5;
49+
} & VersionBeta);
50+
51+
export type VersionAlpha = {
52+
version: 1;
53+
alpha_field: string;
54+
};
55+
56+
export type VersionBeta = {
57+
version: 2.5;
58+
beta_field: string;
59+
};
60+
61+
export type IntegerAllOfBase = {
62+
kind: number;
63+
};
64+
65+
export type IntegerAllOfChildA = Omit<IntegerAllOfBase, 'kind'> & {
66+
child_a_field: string;
67+
kind: 1;
68+
};
69+
70+
export type IntegerAllOfChildB = Omit<IntegerAllOfBase, 'kind'> & {
71+
child_b_field: string;
72+
kind: 2;
73+
};
74+
75+
export type NullableIntegerOneOf = ({
76+
tag: 10;
77+
} & NullableVariantX) | ({
78+
tag: 20;
79+
} & NullableVariantY);
80+
81+
export type NullableVariantX = {
82+
tag: 10 | null;
83+
x_data: string;
84+
};
85+
86+
export type NullableVariantY = {
87+
tag: 20 | null;
88+
y_data: string;
89+
};

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

Lines changed: 85 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import type {
66
SchemaType,
77
SchemaWithRequired,
88
} from '../../../openApi/shared/types/schema';
9-
import { discriminatorValues } from '../../../openApi/shared/utils/discriminator';
9+
import {
10+
convertDiscriminatorValue,
11+
type DiscriminatorPropertyType,
12+
discriminatorValues,
13+
} from '../../../openApi/shared/utils/discriminator';
1014
import { isTopLevelComponent, refToName } from '../../../utils/ref';
1115
import type { ReferenceObject, SchemaObject } from '../types/spec';
1216

@@ -27,6 +31,52 @@ export const getSchemaType = ({
2731
return;
2832
};
2933

34+
/**
35+
* Finds the type of a discriminator property by looking it up in the provided schemas.
36+
* Searches through properties and allOf chains to find the property definition.
37+
*/
38+
const findDiscriminatorPropertyType = ({
39+
context,
40+
propertyName,
41+
schemas,
42+
}: {
43+
context: Context;
44+
propertyName: string;
45+
schemas: ReadonlyArray<SchemaObject | ReferenceObject>;
46+
}): DiscriminatorPropertyType => {
47+
for (const schema of schemas) {
48+
const resolved = '$ref' in schema ? context.resolveRef<SchemaObject>(schema.$ref) : schema;
49+
50+
// Check direct properties
51+
const property = resolved.properties?.[propertyName];
52+
if (property) {
53+
const resolvedProperty =
54+
'$ref' in property ? context.resolveRef<SchemaObject>(property.$ref) : property;
55+
if (
56+
resolvedProperty.type === 'boolean' ||
57+
resolvedProperty.type === 'integer' ||
58+
resolvedProperty.type === 'number'
59+
) {
60+
return resolvedProperty.type;
61+
}
62+
}
63+
64+
// Check allOf chains
65+
if (resolved.allOf) {
66+
const foundType = findDiscriminatorPropertyType({
67+
context,
68+
propertyName,
69+
schemas: resolved.allOf,
70+
});
71+
if (foundType !== 'string') {
72+
return foundType;
73+
}
74+
}
75+
}
76+
77+
return 'string';
78+
};
79+
3080
/**
3181
* Recursively finds discriminators in a schema, including nested allOf compositions.
3282
* This is needed when a schema extends another schema via allOf, and that parent
@@ -482,10 +532,16 @@ const parseAllOf = ({
482532
// Use allValues if we found children, otherwise use the original values
483533
const finalValues = allValues.length > 0 ? allValues : values;
484534

485-
const valueSchemas: ReadonlyArray<IR.SchemaObject> = finalValues.map((value) => ({
486-
const: value,
487-
type: 'string',
488-
}));
535+
// Detect the actual type of the discriminator property
536+
const propertyType = findDiscriminatorPropertyType({
537+
context,
538+
propertyName: discriminator.propertyName,
539+
schemas: compositionSchemas,
540+
});
541+
542+
const valueSchemas: ReadonlyArray<IR.SchemaObject> = finalValues.map((value) =>
543+
convertDiscriminatorValue(value, propertyType),
544+
);
489545

490546
const discriminatorProperty: IR.SchemaObject =
491547
valueSchemas.length > 1
@@ -674,6 +730,14 @@ const parseAnyOf = ({
674730

675731
const compositionSchemas = schema.anyOf;
676732

733+
const discriminatorPropertyType = schema.discriminator
734+
? findDiscriminatorPropertyType({
735+
context,
736+
propertyName: schema.discriminator.propertyName,
737+
schemas: compositionSchemas,
738+
})
739+
: undefined;
740+
677741
for (const compositionSchema of compositionSchemas) {
678742
let irCompositionSchema = schemaToIrSchema({
679743
context,
@@ -684,10 +748,10 @@ const parseAnyOf = ({
684748
// `$ref` should be defined with discriminators
685749
if (schema.discriminator && irCompositionSchema.$ref != null) {
686750
const values = discriminatorValues(irCompositionSchema.$ref, schema.discriminator.mapping);
687-
const valueSchemas: ReadonlyArray<IR.SchemaObject> = values.map((value) => ({
688-
const: value,
689-
type: 'string',
690-
}));
751+
752+
const valueSchemas: ReadonlyArray<IR.SchemaObject> = values.map((value) =>
753+
convertDiscriminatorValue(value, discriminatorPropertyType!),
754+
);
691755
const irDiscriminatorSchema: IR.SchemaObject = {
692756
properties: {
693757
[schema.discriminator.propertyName]:
@@ -834,6 +898,14 @@ const parseOneOf = ({
834898

835899
const compositionSchemas = schema.oneOf;
836900

901+
const discriminatorPropertyType = schema.discriminator
902+
? findDiscriminatorPropertyType({
903+
context,
904+
propertyName: schema.discriminator.propertyName,
905+
schemas: compositionSchemas,
906+
})
907+
: undefined;
908+
837909
for (const compositionSchema of compositionSchemas) {
838910
let irCompositionSchema = schemaToIrSchema({
839911
context,
@@ -844,10 +916,10 @@ const parseOneOf = ({
844916
// `$ref` should be defined with discriminators
845917
if (schema.discriminator && irCompositionSchema.$ref != null) {
846918
const values = discriminatorValues(irCompositionSchema.$ref, schema.discriminator.mapping);
847-
const valueSchemas: ReadonlyArray<IR.SchemaObject> = values.map((value) => ({
848-
const: value,
849-
type: 'string',
850-
}));
919+
920+
const valueSchemas: ReadonlyArray<IR.SchemaObject> = values.map((value) =>
921+
convertDiscriminatorValue(value, discriminatorPropertyType!),
922+
);
851923
const irDiscriminatorSchema: IR.SchemaObject = {
852924
properties: {
853925
[schema.discriminator.propertyName]:

0 commit comments

Comments
 (0)