Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wicked-rings-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hey-api/shared": patch
---

Support non-string discriminator property types (boolean, integer, number)
7 changes: 7 additions & 0 deletions packages/openapi-ts-tests/main/test/3.0.x.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,13 @@ describe(`OpenAPI ${version}`, () => {
}),
description: 'handles nested allOf with discriminators',
},
{
config: createConfig({
input: 'discriminator-non-string.yaml',
output: 'discriminator-non-string',
}),
description: 'handles non-string discriminator property types',
},
{
config: createConfig({
input: 'enum-escape.json',
Expand Down
7 changes: 7 additions & 0 deletions packages/openapi-ts-tests/main/test/3.1.x.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,13 @@ describe(`OpenAPI ${version}`, () => {
}),
description: 'handles nested allOf with discriminators',
},
{
config: createConfig({
input: 'discriminator-non-string.yaml',
output: 'discriminator-non-string',
}),
description: 'handles non-string discriminator property types',
},
{
config: createConfig({
input: 'discriminator-one-of-read-write.yaml',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This file is auto-generated by @hey-api/openapi-ts

export type { AutoConfig, BooleanAnyOf, BooleanOneOf, ClientOptions, CustomConfig, IntegerAllOfBase, IntegerAllOfChildA, IntegerAllOfChildB, IntegerOneOf, NumberOneOf, TypeOne, TypeTwo, VersionAlpha, VersionBeta } from './types.gen';
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// This file is auto-generated by @hey-api/openapi-ts

export type ClientOptions = {
baseUrl: `${string}://${string}` | (string & {});
};

export type BooleanOneOf = ({
use_custom: false;
} & AutoConfig) | ({
use_custom: true;
} & CustomConfig);

export type AutoConfig = {
use_custom: boolean;
auto_setting: string;
};

export type CustomConfig = {
use_custom: boolean;
custom_value: number;
};

export type BooleanAnyOf = ({
use_custom?: false;
} & AutoConfig) | ({
use_custom?: true;
} & CustomConfig);

export type IntegerOneOf = ({
type_id: 1;
} & TypeOne) | ({
type_id: 2;
} & TypeTwo);

export type TypeOne = {
type_id: number;
one_data: string;
};

export type TypeTwo = {
type_id: number;
two_data: string;
};

export type NumberOneOf = ({
version: 1;
} & VersionAlpha) | ({
version: 2.5;
} & VersionBeta);

export type VersionAlpha = {
version: number;
alpha_field: string;
};

export type VersionBeta = {
version: number;
beta_field: string;
};

export type IntegerAllOfBase = {
kind: number;
};

export type IntegerAllOfChildA = Omit<IntegerAllOfBase, 'kind'> & {
child_a_field: string;
kind: 1;
};

export type IntegerAllOfChildB = Omit<IntegerAllOfBase, 'kind'> & {
child_b_field: string;
kind: 2;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This file is auto-generated by @hey-api/openapi-ts

export type { AutoConfig, BooleanAnyOf, BooleanOneOf, ClientOptions, CustomConfig, IntegerAllOfBase, IntegerAllOfChildA, IntegerAllOfChildB, IntegerOneOf, NullableIntegerOneOf, NullableVariantX, NullableVariantY, NumberOneOf, TypeOne, TypeTwo, VersionAlpha, VersionBeta } from './types.gen';
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// This file is auto-generated by @hey-api/openapi-ts

export type ClientOptions = {
baseUrl: `${string}://${string}` | (string & {});
};

export type BooleanOneOf = ({
use_custom: false;
} & AutoConfig) | ({
use_custom: true;
} & CustomConfig);

export type AutoConfig = {
use_custom: false;
auto_setting: string;
};

export type CustomConfig = {
use_custom: true;
custom_value: number;
};

export type BooleanAnyOf = ({
use_custom?: false;
} & AutoConfig) | ({
use_custom?: true;
} & CustomConfig);

export type IntegerOneOf = ({
type_id: 1;
} & TypeOne) | ({
type_id: 2;
} & TypeTwo);

export type TypeOne = {
type_id: 1;
one_data: string;
};

export type TypeTwo = {
type_id: 2;
two_data: string;
};

export type NumberOneOf = ({
version: 1;
} & VersionAlpha) | ({
version: 2.5;
} & VersionBeta);

export type VersionAlpha = {
version: 1;
alpha_field: string;
};

export type VersionBeta = {
version: 2.5;
beta_field: string;
};

export type IntegerAllOfBase = {
kind: number;
};

export type IntegerAllOfChildA = Omit<IntegerAllOfBase, 'kind'> & {
child_a_field: string;
kind: 1;
};

export type IntegerAllOfChildB = Omit<IntegerAllOfBase, 'kind'> & {
child_b_field: string;
kind: 2;
};

export type NullableIntegerOneOf = ({
tag: 10;
} & NullableVariantX) | ({
tag: 20;
} & NullableVariantY);

export type NullableVariantX = {
tag: 10 | null;
x_data: string;
};

export type NullableVariantY = {
tag: 20 | null;
y_data: string;
};
98 changes: 85 additions & 13 deletions packages/shared/src/openApi/3.0.x/parser/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import type {
SchemaType,
SchemaWithRequired,
} from '../../../openApi/shared/types/schema';
import { discriminatorValues } from '../../../openApi/shared/utils/discriminator';
import {
convertDiscriminatorValue,
type DiscriminatorPropertyType,
discriminatorValues,
} from '../../../openApi/shared/utils/discriminator';
import { isTopLevelComponent, refToName } from '../../../utils/ref';
import type { ReferenceObject, SchemaObject } from '../types/spec';

Expand All @@ -27,6 +31,52 @@ export const getSchemaType = ({
return;
};

/**
* Finds the type of a discriminator property by looking it up in the provided schemas.
* Searches through properties and allOf chains to find the property definition.
*/
const findDiscriminatorPropertyType = ({
context,
propertyName,
schemas,
}: {
context: Context;
propertyName: string;
schemas: ReadonlyArray<SchemaObject | ReferenceObject>;
}): DiscriminatorPropertyType => {
for (const schema of schemas) {
const resolved = '$ref' in schema ? context.resolveRef<SchemaObject>(schema.$ref) : schema;

// Check direct properties
const property = resolved.properties?.[propertyName];
if (property) {
const resolvedProperty =
'$ref' in property ? context.resolveRef<SchemaObject>(property.$ref) : property;
if (
resolvedProperty.type === 'boolean' ||
resolvedProperty.type === 'integer' ||
resolvedProperty.type === 'number'
) {
return resolvedProperty.type;
}
}

// Check allOf chains
if (resolved.allOf) {
const foundType = findDiscriminatorPropertyType({
context,
propertyName,
schemas: resolved.allOf,
});
if (foundType !== 'string') {
return foundType;
}
}
}

return 'string';
};

/**
* Recursively finds discriminators in a schema, including nested allOf compositions.
* This is needed when a schema extends another schema via allOf, and that parent
Expand Down Expand Up @@ -482,10 +532,16 @@ const parseAllOf = ({
// Use allValues if we found children, otherwise use the original values
const finalValues = allValues.length > 0 ? allValues : values;

const valueSchemas: ReadonlyArray<IR.SchemaObject> = finalValues.map((value) => ({
const: value,
type: 'string',
}));
// Detect the actual type of the discriminator property
const propertyType = findDiscriminatorPropertyType({
context,
propertyName: discriminator.propertyName,
schemas: compositionSchemas,
});

const valueSchemas: ReadonlyArray<IR.SchemaObject> = finalValues.map((value) =>
convertDiscriminatorValue(value, propertyType),
);

const discriminatorProperty: IR.SchemaObject =
valueSchemas.length > 1
Expand Down Expand Up @@ -674,6 +730,14 @@ const parseAnyOf = ({

const compositionSchemas = schema.anyOf;

const discriminatorPropertyType = schema.discriminator
? findDiscriminatorPropertyType({
context,
propertyName: schema.discriminator.propertyName,
schemas: compositionSchemas,
})
: undefined;

for (const compositionSchema of compositionSchemas) {
let irCompositionSchema = schemaToIrSchema({
context,
Expand All @@ -684,10 +748,10 @@ const parseAnyOf = ({
// `$ref` should be defined with discriminators
if (schema.discriminator && irCompositionSchema.$ref != null) {
const values = discriminatorValues(irCompositionSchema.$ref, schema.discriminator.mapping);
const valueSchemas: ReadonlyArray<IR.SchemaObject> = values.map((value) => ({
const: value,
type: 'string',
}));

const valueSchemas: ReadonlyArray<IR.SchemaObject> = values.map((value) =>
convertDiscriminatorValue(value, discriminatorPropertyType!),
);
const irDiscriminatorSchema: IR.SchemaObject = {
properties: {
[schema.discriminator.propertyName]:
Expand Down Expand Up @@ -834,6 +898,14 @@ const parseOneOf = ({

const compositionSchemas = schema.oneOf;

const discriminatorPropertyType = schema.discriminator
? findDiscriminatorPropertyType({
context,
propertyName: schema.discriminator.propertyName,
schemas: compositionSchemas,
})
: undefined;

for (const compositionSchema of compositionSchemas) {
let irCompositionSchema = schemaToIrSchema({
context,
Expand All @@ -844,10 +916,10 @@ const parseOneOf = ({
// `$ref` should be defined with discriminators
if (schema.discriminator && irCompositionSchema.$ref != null) {
const values = discriminatorValues(irCompositionSchema.$ref, schema.discriminator.mapping);
const valueSchemas: ReadonlyArray<IR.SchemaObject> = values.map((value) => ({
const: value,
type: 'string',
}));

const valueSchemas: ReadonlyArray<IR.SchemaObject> = values.map((value) =>
convertDiscriminatorValue(value, discriminatorPropertyType!),
);
const irDiscriminatorSchema: IR.SchemaObject = {
properties: {
[schema.discriminator.propertyName]:
Expand Down
Loading
Loading