diff --git a/src/components/Schema/ObjectSchema.tsx b/src/components/Schema/ObjectSchema.tsx index b59ea989f5..ed7c822679 100644 --- a/src/components/Schema/ObjectSchema.tsx +++ b/src/components/Schema/ObjectSchema.tsx @@ -49,7 +49,17 @@ export const ObjectSchema = observer( const expandByDefault = (expandSingleSchemaField && filteredFields.length === 1) || schemasExpansionLevel >= level!; - return ( + // The discriminator dropdown is normally attached to the field row whose name + // matches the discriminator property. When the variant schemas don't declare that + // property (e.g. a `oneOf` + `discriminator` written without `allOf` inheritance, as + // commonly emitted by code generators), no field matches and the selector would + // silently disappear, leaving only the first variant visible. In that case render the + // dropdown standalone so the polymorphic variants stay switchable. + const hasDiscriminatorField = + !!discriminator && filteredFields.some(field => field.name === discriminator.fieldName); + const showStandaloneDiscriminator = !!discriminator && !hasDiscriminatorField; + + const propertiesTable = ( {showTitle && {title}} @@ -83,5 +93,19 @@ export const ObjectSchema = observer( ); + + if (!showStandaloneDiscriminator) { + return propertiesTable; + } + + return ( + <> + s.title) ?? []} + /> + {propertiesTable} + + ); }, ); diff --git a/src/components/__tests__/DiscriminatorDropdown.test.tsx b/src/components/__tests__/DiscriminatorDropdown.test.tsx index 1a625b94b7..c35d91cb98 100644 --- a/src/components/__tests__/DiscriminatorDropdown.test.tsx +++ b/src/components/__tests__/DiscriminatorDropdown.test.tsx @@ -8,9 +8,11 @@ import * as React from 'react'; import { filterPropsDeep } from '../../utils/test-utils'; import { ObjectSchema, Schema } from '../'; +import { DiscriminatorDropdown } from '../Schema/DiscriminatorDropdown'; import { OpenAPIParser, SchemaModel } from '../../services'; import { RedocNormalizedOptions } from '../../services/RedocNormalizedOptions'; import * as simpleDiscriminatorFixture from './fixtures/simple-discriminator.json'; +import * as oneOfDiscriminatorNoPropFixture from './fixtures/oneof-discriminator-no-prop.json'; const options = new RedocNormalizedOptions({}); describe('Components', () => { @@ -52,6 +54,60 @@ describe('Components', () => { ); expect(filterPropsDeep(toJson(schemaView), ['field.schema.options'])).toMatchSnapshot(); }); + + it('should still render the dropdown when variants do not declare the discriminator property', () => { + // oneOf + discriminator where the variant schemas (CardPaymentInput, CashPaymentInput) + // neither declare `__typename` nor inherit it via allOf. Previously the selector was + // attached only to a matching field row, so it silently disappeared and only the first + // variant was shown. The dropdown must now render standalone. + const parser = new OpenAPIParser(oneOfDiscriminatorNoPropFixture, undefined, options); + const schema = new SchemaModel( + parser, + { $ref: '#/components/schemas/PaymentInput' }, + '#/components/schemas/PaymentInput', + options, + ); + + expect(schema.discriminatorProp).toEqual('__typename'); + expect(schema.oneOf).toHaveLength(2); + // sanity: the active variant genuinely has no field named like the discriminator + expect(schema.oneOf![0].fields?.some(f => f.name === schema.discriminatorProp)).toBe(false); + + const schemaView = shallow( + , + ); + expect(schemaView.find(DiscriminatorDropdown)).toHaveLength(1); + }); + + it('should not render a standalone dropdown when the variant declares the discriminator property', () => { + // Regression guard for the standard allOf-inheritance pattern: the discriminator field + // exists on the variant, so the selector stays inline on that field row and no extra + // standalone dropdown is added at the ObjectSchema level. + const parser = new OpenAPIParser(simpleDiscriminatorFixture, undefined, options); + const schema = new SchemaModel( + parser, + { $ref: '#/components/schemas/Pet' }, + '#/components/schemas/Pet', + options, + ); + const schemaView = shallow( + , + ); + // the inline dropdown lives inside a render prop and is not a direct child here + expect(schemaView.find(DiscriminatorDropdown)).toHaveLength(0); + }); }); }); }); diff --git a/src/components/__tests__/fixtures/oneof-discriminator-no-prop.json b/src/components/__tests__/fixtures/oneof-discriminator-no-prop.json new file mode 100644 index 0000000000..a9c3c0d207 --- /dev/null +++ b/src/components/__tests__/fixtures/oneof-discriminator-no-prop.json @@ -0,0 +1,39 @@ +{ + "openapi": "3.1.0", + "components": { + "schemas": { + "PaymentInput": { + "type": "object", + "properties": { + "amount": { "type": "integer" } + }, + "oneOf": [ + { "$ref": "#/components/schemas/CardPaymentInput" }, + { "$ref": "#/components/schemas/CashPaymentInput" } + ], + "required": ["amount"], + "discriminator": { + "propertyName": "__typename", + "mapping": { + "CardPayment": "#/components/schemas/CardPaymentInput", + "CashPayment": "#/components/schemas/CashPaymentInput" + } + } + }, + "CardPaymentInput": { + "type": "object", + "properties": { + "last4": { "type": "string" } + }, + "required": ["last4"] + }, + "CashPaymentInput": { + "type": "object", + "properties": { + "tendered": { "type": "integer" } + }, + "required": ["tendered"] + } + } + } +}