From dd0be9c6ca93552c3367ca8e8ba785381295a112 Mon Sep 17 00:00:00 2001 From: Sipan Petrosyan Date: Thu, 12 Feb 2026 21:19:17 +0000 Subject: [PATCH 1/3] 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. --- .changeset/wicked-rings-march.md | 5 + .../openapi-ts-tests/main/test/3.0.x.test.ts | 7 + .../openapi-ts-tests/main/test/3.1.x.test.ts | 7 + .../3.0.x/discriminator-non-string/index.ts | 3 + .../discriminator-non-string/types.gen.ts | 73 +++++++ .../3.1.x/discriminator-non-string/index.ts | 3 + .../discriminator-non-string/types.gen.ts | 89 ++++++++ .../shared/src/openApi/2.0.x/parser/schema.ts | 68 +++++- .../shared/src/openApi/3.0.x/parser/schema.ts | 98 +++++++-- .../shared/src/openApi/3.1.x/parser/schema.ts | 103 ++++++++-- .../src/openApi/shared/utils/discriminator.ts | 78 +++++++ specs/3.0.x/discriminator-non-string.yaml | 155 ++++++++++++++ specs/3.1.x/discriminator-non-string.yaml | 194 ++++++++++++++++++ 13 files changed, 852 insertions(+), 31 deletions(-) create mode 100644 .changeset/wicked-rings-march.md create mode 100644 packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/discriminator-non-string/index.ts create mode 100644 packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/discriminator-non-string/types.gen.ts create mode 100644 packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-non-string/index.ts create mode 100644 packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-non-string/types.gen.ts create mode 100644 specs/3.0.x/discriminator-non-string.yaml create mode 100644 specs/3.1.x/discriminator-non-string.yaml diff --git a/.changeset/wicked-rings-march.md b/.changeset/wicked-rings-march.md new file mode 100644 index 0000000000..91db8ede9d --- /dev/null +++ b/.changeset/wicked-rings-march.md @@ -0,0 +1,5 @@ +--- +"@hey-api/shared": patch +--- + +Support non-string discriminator property types (boolean, integer, number) diff --git a/packages/openapi-ts-tests/main/test/3.0.x.test.ts b/packages/openapi-ts-tests/main/test/3.0.x.test.ts index b8a28a5f91..77ed4def16 100644 --- a/packages/openapi-ts-tests/main/test/3.0.x.test.ts +++ b/packages/openapi-ts-tests/main/test/3.0.x.test.ts @@ -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', diff --git a/packages/openapi-ts-tests/main/test/3.1.x.test.ts b/packages/openapi-ts-tests/main/test/3.1.x.test.ts index e45d157957..1bee7b8cb6 100644 --- a/packages/openapi-ts-tests/main/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/main/test/3.1.x.test.ts @@ -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', diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/discriminator-non-string/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/discriminator-non-string/index.ts new file mode 100644 index 0000000000..dace937e59 --- /dev/null +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/discriminator-non-string/index.ts @@ -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'; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/discriminator-non-string/types.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/discriminator-non-string/types.gen.ts new file mode 100644 index 0000000000..c5749a9d36 --- /dev/null +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/discriminator-non-string/types.gen.ts @@ -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 & { + child_a_field: string; + kind: 1; +}; + +export type IntegerAllOfChildB = Omit & { + child_b_field: string; + kind: 2; +}; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-non-string/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-non-string/index.ts new file mode 100644 index 0000000000..ea395f1ee4 --- /dev/null +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-non-string/index.ts @@ -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'; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-non-string/types.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-non-string/types.gen.ts new file mode 100644 index 0000000000..0ca4093758 --- /dev/null +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/discriminator-non-string/types.gen.ts @@ -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 & { + child_a_field: string; + kind: 1; +}; + +export type IntegerAllOfChildB = Omit & { + 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; +}; diff --git a/packages/shared/src/openApi/2.0.x/parser/schema.ts b/packages/shared/src/openApi/2.0.x/parser/schema.ts index d3f2cf55c8..05defd3016 100644 --- a/packages/shared/src/openApi/2.0.x/parser/schema.ts +++ b/packages/shared/src/openApi/2.0.x/parser/schema.ts @@ -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 { SchemaObject } from '../types/spec'; @@ -27,6 +31,53 @@ 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; +}): DiscriminatorPropertyType => { + for (const schema of schemas) { + const resolved = schema.$ref ? context.resolveRef(schema.$ref) : schema; + + // Check direct properties + const property = resolved.properties?.[propertyName]; + if (property) { + const resolvedProperty = property.$ref + ? context.resolveRef(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'; +}; + const parseSchemaJsDoc = ({ irSchema, schema, @@ -336,10 +387,17 @@ const parseAllOf = ({ // `$ref` should be passed from the root `parseSchema()` call if (ref.discriminator && state.$ref) { const values = discriminatorValues(state.$ref); - const valueSchemas: ReadonlyArray = values.map((value) => ({ - const: value, - type: 'string', - })); + + // Detect the actual type of the discriminator property + const propertyType = findDiscriminatorPropertyType({ + context, + propertyName: ref.discriminator, + schemas: [ref], + }); + + const valueSchemas: ReadonlyArray = values.map((value) => + convertDiscriminatorValue(value, propertyType), + ); const irDiscriminatorSchema: IR.SchemaObject = { properties: { [ref.discriminator]: diff --git a/packages/shared/src/openApi/3.0.x/parser/schema.ts b/packages/shared/src/openApi/3.0.x/parser/schema.ts index f805b8b1f1..2800f7e147 100644 --- a/packages/shared/src/openApi/3.0.x/parser/schema.ts +++ b/packages/shared/src/openApi/3.0.x/parser/schema.ts @@ -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'; @@ -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; +}): DiscriminatorPropertyType => { + for (const schema of schemas) { + const resolved = '$ref' in schema ? context.resolveRef(schema.$ref) : schema; + + // Check direct properties + const property = resolved.properties?.[propertyName]; + if (property) { + const resolvedProperty = + '$ref' in property ? context.resolveRef(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 @@ -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 = 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 = finalValues.map((value) => + convertDiscriminatorValue(value, propertyType), + ); const discriminatorProperty: IR.SchemaObject = valueSchemas.length > 1 @@ -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, @@ -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 = values.map((value) => ({ - const: value, - type: 'string', - })); + + const valueSchemas: ReadonlyArray = values.map((value) => + convertDiscriminatorValue(value, discriminatorPropertyType!), + ); const irDiscriminatorSchema: IR.SchemaObject = { properties: { [schema.discriminator.propertyName]: @@ -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, @@ -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 = values.map((value) => ({ - const: value, - type: 'string', - })); + + const valueSchemas: ReadonlyArray = values.map((value) => + convertDiscriminatorValue(value, discriminatorPropertyType!), + ); const irDiscriminatorSchema: IR.SchemaObject = { properties: { [schema.discriminator.propertyName]: diff --git a/packages/shared/src/openApi/3.1.x/parser/schema.ts b/packages/shared/src/openApi/3.1.x/parser/schema.ts index 0cad93884b..a488dc991f 100644 --- a/packages/shared/src/openApi/3.1.x/parser/schema.ts +++ b/packages/shared/src/openApi/3.1.x/parser/schema.ts @@ -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 { SchemaObject } from '../types/spec'; @@ -31,6 +35,57 @@ export const getSchemaTypes = ({ 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; +}): DiscriminatorPropertyType => { + for (const schema of schemas) { + const resolved = schema.$ref ? context.resolveRef(schema.$ref) : schema; + + // Check direct properties + const property = resolved.properties?.[propertyName]; + if (property) { + const resolvedProperty = property.$ref + ? context.resolveRef(property.$ref) + : property; + // Handle both single type and array of types (3.1.x supports type arrays) + const propertyTypes = Array.isArray(resolvedProperty.type) + ? resolvedProperty.type + : resolvedProperty.type + ? [resolvedProperty.type] + : []; + for (const propType of propertyTypes) { + if (propType === 'boolean' || propType === 'integer' || propType === 'number') { + return propType; + } + } + } + + // 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 @@ -564,10 +619,16 @@ const parseAllOf = ({ // Use allValues if we found children, otherwise use the original values const finalValues = allValues.length > 0 ? allValues : values; - const valueSchemas: ReadonlyArray = 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 = finalValues.map((value) => + convertDiscriminatorValue(value, propertyType), + ); const discriminatorProperty: IR.SchemaObject = valueSchemas.length > 1 @@ -743,6 +804,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, @@ -753,10 +822,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 = values.map((value) => ({ - const: value, - type: 'string', - })); + + const valueSchemas: ReadonlyArray = values.map((value) => + convertDiscriminatorValue(value, discriminatorPropertyType!), + ); const irDiscriminatorSchema: IR.SchemaObject = { properties: { [schema.discriminator.propertyName]: @@ -894,6 +963,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, @@ -904,10 +981,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 = values.map((value) => ({ - const: value, - type: 'string', - })); + + const valueSchemas: ReadonlyArray = values.map((value) => + convertDiscriminatorValue(value, discriminatorPropertyType!), + ); const irDiscriminatorSchema: IR.SchemaObject = { properties: { [schema.discriminator.propertyName]: diff --git a/packages/shared/src/openApi/shared/utils/discriminator.ts b/packages/shared/src/openApi/shared/utils/discriminator.ts index 836af5986a..68f09219ce 100644 --- a/packages/shared/src/openApi/shared/utils/discriminator.ts +++ b/packages/shared/src/openApi/shared/utils/discriminator.ts @@ -1,5 +1,83 @@ +import type { IR } from '../../../ir/types'; import { refToName } from '../../../utils/ref'; +/** + * Supported types for discriminator properties. + */ +export type DiscriminatorPropertyType = 'boolean' | 'integer' | 'number' | 'string'; + +/** + * Converts a string discriminator mapping value to the appropriate type based on + * the actual property type in the schema. + * + * OpenAPI discriminator mappings always use string keys, but the actual discriminator + * property may be a boolean, number, or integer. This function converts the string + * key to the correct runtime value and IR type. + */ +export const convertDiscriminatorValue = ( + value: string, + propertyType: DiscriminatorPropertyType, +): { const: IR.SchemaObject['const']; type: IR.SchemaObject['type'] } => { + switch (propertyType) { + case 'boolean': { + const lowerValue = value.toLowerCase(); + if (lowerValue !== 'true' && lowerValue !== 'false') { + console.warn( + '🚨', + `non-boolean discriminator mapping value "${value}" for boolean property, falling back to string`, + ); + return { + const: value, + type: 'string', + }; + } + return { + const: lowerValue === 'true', + type: 'boolean', + }; + } + case 'integer': { + const parsed = parseInt(value, 10); + if (Number.isNaN(parsed)) { + console.warn( + '🚨', + `non-numeric discriminator mapping value "${value}" for integer property, falling back to string`, + ); + return { + const: value, + type: 'string', + }; + } + return { + const: parsed, + type: 'integer', + }; + } + case 'number': { + const parsed = parseFloat(value); + if (Number.isNaN(parsed)) { + console.warn( + '🚨', + `non-numeric discriminator mapping value "${value}" for number property, falling back to string`, + ); + return { + const: value, + type: 'string', + }; + } + return { + const: parsed, + type: 'number', + }; + } + default: + return { + const: value, + type: 'string', + }; + } +}; + export const discriminatorValues = ( $ref: string, mapping?: Record, diff --git a/specs/3.0.x/discriminator-non-string.yaml b/specs/3.0.x/discriminator-non-string.yaml new file mode 100644 index 0000000000..bf82c217f3 --- /dev/null +++ b/specs/3.0.x/discriminator-non-string.yaml @@ -0,0 +1,155 @@ +openapi: 3.0.3 +info: + title: Non-string discriminator test + version: 1 +components: + schemas: + # --- Boolean discriminator (oneOf) --- + BooleanOneOf: + oneOf: + - $ref: '#/components/schemas/AutoConfig' + - $ref: '#/components/schemas/CustomConfig' + discriminator: + propertyName: use_custom + mapping: + 'False': '#/components/schemas/AutoConfig' + 'True': '#/components/schemas/CustomConfig' + + AutoConfig: + type: object + required: + - use_custom + - auto_setting + properties: + use_custom: + type: boolean + const: false + auto_setting: + type: string + + CustomConfig: + type: object + required: + - use_custom + - custom_value + properties: + use_custom: + type: boolean + const: true + custom_value: + type: integer + + # --- Boolean discriminator (anyOf) --- + BooleanAnyOf: + anyOf: + - $ref: '#/components/schemas/AutoConfig' + - $ref: '#/components/schemas/CustomConfig' + discriminator: + propertyName: use_custom + mapping: + 'False': '#/components/schemas/AutoConfig' + 'True': '#/components/schemas/CustomConfig' + + # --- Integer discriminator (oneOf) --- + IntegerOneOf: + oneOf: + - $ref: '#/components/schemas/TypeOne' + - $ref: '#/components/schemas/TypeTwo' + discriminator: + propertyName: type_id + mapping: + '1': '#/components/schemas/TypeOne' + '2': '#/components/schemas/TypeTwo' + + TypeOne: + type: object + required: + - type_id + - one_data + properties: + type_id: + type: integer + const: 1 + one_data: + type: string + + TypeTwo: + type: object + required: + - type_id + - two_data + properties: + type_id: + type: integer + const: 2 + two_data: + type: string + + # --- Number (float) discriminator (oneOf) --- + NumberOneOf: + oneOf: + - $ref: '#/components/schemas/VersionAlpha' + - $ref: '#/components/schemas/VersionBeta' + discriminator: + propertyName: version + mapping: + '1.0': '#/components/schemas/VersionAlpha' + '2.5': '#/components/schemas/VersionBeta' + + VersionAlpha: + type: object + required: + - version + - alpha_field + properties: + version: + type: number + const: 1.0 + alpha_field: + type: string + + VersionBeta: + type: object + required: + - version + - beta_field + properties: + version: + type: number + const: 2.5 + beta_field: + type: string + + # --- Integer discriminator (allOf) --- + IntegerAllOfBase: + type: object + required: + - kind + properties: + kind: + type: integer + discriminator: + propertyName: kind + mapping: + '1': '#/components/schemas/IntegerAllOfChildA' + '2': '#/components/schemas/IntegerAllOfChildB' + + IntegerAllOfChildA: + allOf: + - $ref: '#/components/schemas/IntegerAllOfBase' + - type: object + required: + - child_a_field + properties: + child_a_field: + type: string + + IntegerAllOfChildB: + allOf: + - $ref: '#/components/schemas/IntegerAllOfBase' + - type: object + required: + - child_b_field + properties: + child_b_field: + type: string diff --git a/specs/3.1.x/discriminator-non-string.yaml b/specs/3.1.x/discriminator-non-string.yaml new file mode 100644 index 0000000000..78c6b0e5c6 --- /dev/null +++ b/specs/3.1.x/discriminator-non-string.yaml @@ -0,0 +1,194 @@ +openapi: 3.1.0 +info: + title: Non-string discriminator test + version: 1 +components: + schemas: + # --- Boolean discriminator (oneOf) --- + BooleanOneOf: + oneOf: + - $ref: '#/components/schemas/AutoConfig' + - $ref: '#/components/schemas/CustomConfig' + discriminator: + propertyName: use_custom + mapping: + 'False': '#/components/schemas/AutoConfig' + 'True': '#/components/schemas/CustomConfig' + + AutoConfig: + type: object + required: + - use_custom + - auto_setting + properties: + use_custom: + type: boolean + const: false + auto_setting: + type: string + + CustomConfig: + type: object + required: + - use_custom + - custom_value + properties: + use_custom: + type: boolean + const: true + custom_value: + type: integer + + # --- Boolean discriminator (anyOf) --- + BooleanAnyOf: + anyOf: + - $ref: '#/components/schemas/AutoConfig' + - $ref: '#/components/schemas/CustomConfig' + discriminator: + propertyName: use_custom + mapping: + 'False': '#/components/schemas/AutoConfig' + 'True': '#/components/schemas/CustomConfig' + + # --- Integer discriminator (oneOf) --- + IntegerOneOf: + oneOf: + - $ref: '#/components/schemas/TypeOne' + - $ref: '#/components/schemas/TypeTwo' + discriminator: + propertyName: type_id + mapping: + '1': '#/components/schemas/TypeOne' + '2': '#/components/schemas/TypeTwo' + + TypeOne: + type: object + required: + - type_id + - one_data + properties: + type_id: + type: integer + const: 1 + one_data: + type: string + + TypeTwo: + type: object + required: + - type_id + - two_data + properties: + type_id: + type: integer + const: 2 + two_data: + type: string + + # --- Number (float) discriminator (oneOf) --- + NumberOneOf: + oneOf: + - $ref: '#/components/schemas/VersionAlpha' + - $ref: '#/components/schemas/VersionBeta' + discriminator: + propertyName: version + mapping: + '1.0': '#/components/schemas/VersionAlpha' + '2.5': '#/components/schemas/VersionBeta' + + VersionAlpha: + type: object + required: + - version + - alpha_field + properties: + version: + type: number + const: 1.0 + alpha_field: + type: string + + VersionBeta: + type: object + required: + - version + - beta_field + properties: + version: + type: number + const: 2.5 + beta_field: + type: string + + # --- Integer discriminator (allOf) --- + IntegerAllOfBase: + type: object + required: + - kind + properties: + kind: + type: integer + discriminator: + propertyName: kind + mapping: + '1': '#/components/schemas/IntegerAllOfChildA' + '2': '#/components/schemas/IntegerAllOfChildB' + + IntegerAllOfChildA: + allOf: + - $ref: '#/components/schemas/IntegerAllOfBase' + - type: object + required: + - child_a_field + properties: + child_a_field: + type: string + + IntegerAllOfChildB: + allOf: + - $ref: '#/components/schemas/IntegerAllOfBase' + - type: object + required: + - child_b_field + properties: + child_b_field: + type: string + + # --- 3.1.x-specific: discriminator property with type array (e.g. nullable integer) --- + NullableIntegerOneOf: + oneOf: + - $ref: '#/components/schemas/NullableVariantX' + - $ref: '#/components/schemas/NullableVariantY' + discriminator: + propertyName: tag + mapping: + '10': '#/components/schemas/NullableVariantX' + '20': '#/components/schemas/NullableVariantY' + + NullableVariantX: + type: object + required: + - tag + - x_data + properties: + tag: + type: + - integer + - 'null' + const: 10 + x_data: + type: string + + NullableVariantY: + type: object + required: + - tag + - y_data + properties: + tag: + type: + - integer + - 'null' + const: 20 + y_data: + type: string From 6715abb37be8409735209b672b5f99eca0e588a7 Mon Sep 17 00:00:00 2001 From: Sipan Petrosyan Date: Sat, 14 Feb 2026 12:08:05 +0000 Subject: [PATCH 2/3] fix typecheck in findDiscriminatorPropertyType 3.1.x --- packages/shared/src/openApi/3.1.x/parser/schema.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/shared/src/openApi/3.1.x/parser/schema.ts b/packages/shared/src/openApi/3.1.x/parser/schema.ts index a488dc991f..f104b86c17 100644 --- a/packages/shared/src/openApi/3.1.x/parser/schema.ts +++ b/packages/shared/src/openApi/3.1.x/parser/schema.ts @@ -53,6 +53,9 @@ const findDiscriminatorPropertyType = ({ // Check direct properties const property = resolved.properties?.[propertyName]; + if (property === true) { + continue; + } if (property) { const resolvedProperty = property.$ref ? context.resolveRef(property.$ref) From 53413efd3fe0196ed3a53b6d9e5d8af03583f1fe Mon Sep 17 00:00:00 2001 From: Sipan Petrosyan Date: Sat, 14 Feb 2026 17:10:13 +0000 Subject: [PATCH 3/3] remove discriminator changes in openapi 2.0.x --- .../shared/src/openApi/2.0.x/parser/schema.ts | 68 ++----------------- 1 file changed, 5 insertions(+), 63 deletions(-) diff --git a/packages/shared/src/openApi/2.0.x/parser/schema.ts b/packages/shared/src/openApi/2.0.x/parser/schema.ts index 05defd3016..d3f2cf55c8 100644 --- a/packages/shared/src/openApi/2.0.x/parser/schema.ts +++ b/packages/shared/src/openApi/2.0.x/parser/schema.ts @@ -6,11 +6,7 @@ import type { SchemaType, SchemaWithRequired, } from '../../../openApi/shared/types/schema'; -import { - convertDiscriminatorValue, - type DiscriminatorPropertyType, - discriminatorValues, -} from '../../../openApi/shared/utils/discriminator'; +import { discriminatorValues } from '../../../openApi/shared/utils/discriminator'; import { isTopLevelComponent, refToName } from '../../../utils/ref'; import type { SchemaObject } from '../types/spec'; @@ -31,53 +27,6 @@ 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; -}): DiscriminatorPropertyType => { - for (const schema of schemas) { - const resolved = schema.$ref ? context.resolveRef(schema.$ref) : schema; - - // Check direct properties - const property = resolved.properties?.[propertyName]; - if (property) { - const resolvedProperty = property.$ref - ? context.resolveRef(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'; -}; - const parseSchemaJsDoc = ({ irSchema, schema, @@ -387,17 +336,10 @@ const parseAllOf = ({ // `$ref` should be passed from the root `parseSchema()` call if (ref.discriminator && state.$ref) { const values = discriminatorValues(state.$ref); - - // Detect the actual type of the discriminator property - const propertyType = findDiscriminatorPropertyType({ - context, - propertyName: ref.discriminator, - schemas: [ref], - }); - - const valueSchemas: ReadonlyArray = values.map((value) => - convertDiscriminatorValue(value, propertyType), - ); + const valueSchemas: ReadonlyArray = values.map((value) => ({ + const: value, + type: 'string', + })); const irDiscriminatorSchema: IR.SchemaObject = { properties: { [ref.discriminator]: