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"]
+ }
+ }
+ }
+}