Skip to content

Commit dd0be9c

Browse files
committed
fix(shared): support non-string discriminator property types
OpenAPI discriminator mappings use string keys, but the actual discriminator property may be boolean, integer, or number. Previously, all discriminator values were hardcoded as type 'string'. This change detects the actual property type from the schema and converts mapping values accordingly.
1 parent a55a5e1 commit dd0be9c

13 files changed

Lines changed: 852 additions & 31 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/2.0.x/parser/schema.ts

Lines changed: 63 additions & 5 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 { SchemaObject } from '../types/spec';
1216

@@ -27,6 +31,53 @@ 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>;
46+
}): DiscriminatorPropertyType => {
47+
for (const schema of schemas) {
48+
const resolved = schema.$ref ? context.resolveRef<SchemaObject>(schema.$ref) : schema;
49+
50+
// Check direct properties
51+
const property = resolved.properties?.[propertyName];
52+
if (property) {
53+
const resolvedProperty = property.$ref
54+
? context.resolveRef<SchemaObject>(property.$ref)
55+
: property;
56+
if (
57+
resolvedProperty.type === 'boolean' ||
58+
resolvedProperty.type === 'integer' ||
59+
resolvedProperty.type === 'number'
60+
) {
61+
return resolvedProperty.type;
62+
}
63+
}
64+
65+
// Check allOf chains
66+
if (resolved.allOf) {
67+
const foundType = findDiscriminatorPropertyType({
68+
context,
69+
propertyName,
70+
schemas: resolved.allOf,
71+
});
72+
if (foundType !== 'string') {
73+
return foundType;
74+
}
75+
}
76+
}
77+
78+
return 'string';
79+
};
80+
3081
const parseSchemaJsDoc = ({
3182
irSchema,
3283
schema,
@@ -336,10 +387,17 @@ const parseAllOf = ({
336387
// `$ref` should be passed from the root `parseSchema()` call
337388
if (ref.discriminator && state.$ref) {
338389
const values = discriminatorValues(state.$ref);
339-
const valueSchemas: ReadonlyArray<IR.SchemaObject> = values.map((value) => ({
340-
const: value,
341-
type: 'string',
342-
}));
390+
391+
// Detect the actual type of the discriminator property
392+
const propertyType = findDiscriminatorPropertyType({
393+
context,
394+
propertyName: ref.discriminator,
395+
schemas: [ref],
396+
});
397+
398+
const valueSchemas: ReadonlyArray<IR.SchemaObject> = values.map((value) =>
399+
convertDiscriminatorValue(value, propertyType),
400+
);
343401
const irDiscriminatorSchema: IR.SchemaObject = {
344402
properties: {
345403
[ref.discriminator]:

0 commit comments

Comments
 (0)