Skip to content

Commit 121d379

Browse files
sserratacursoragent
andcommitted
fix(theme): improve nested discriminator handling and examples
Handle recursively nested discriminators in composed schemas while preserving intentional empty-object rendering, and add demo specs covering deep allOf discriminator chains and explicit empty object properties. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 5b62b95 commit 121d379

3 files changed

Lines changed: 167 additions & 19 deletions

File tree

demo/examples/tests/allOf.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,39 @@ paths:
510510
name:
511511
type: string
512512

513+
/allof-empty-object-properties:
514+
get:
515+
tags:
516+
- allOf
517+
summary: allOf with explicit empty object properties
518+
description: |
519+
Demonstrates an intentional empty object schema (properties: {}) to ensure
520+
the renderer still shows the object shape rather than hiding it.
521+
522+
Schema:
523+
```yaml
524+
type: object
525+
properties:
526+
metadata:
527+
type: object
528+
properties: {}
529+
name:
530+
type: string
531+
```
532+
responses:
533+
"200":
534+
description: Successful response
535+
content:
536+
application/json:
537+
schema:
538+
type: object
539+
properties:
540+
metadata:
541+
type: object
542+
properties: {}
543+
name:
544+
type: string
545+
513546
/allof-multiple-oneof:
514547
post:
515548
tags:

demo/examples/tests/discriminator.yaml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,32 @@ paths:
417417
schema:
418418
$ref: "#/components/schemas/DoublyNestedBase"
419419

420+
/discriminator-deeply-nested-allof:
421+
post:
422+
tags:
423+
- discriminator
424+
summary: Deeply Nested Discriminator in allOf chains
425+
description: |
426+
Tests discriminator discovery through nested allOf chains:
427+
- top-level allOf
428+
- inner allOf
429+
- oneOf with discriminator
430+
431+
This validates recursive discriminator lookup beyond first-level allOf items.
432+
requestBody:
433+
required: true
434+
content:
435+
application/json:
436+
schema:
437+
$ref: "#/components/schemas/DeepNestedDiscriminatorBase"
438+
responses:
439+
"200":
440+
description: Successful response
441+
content:
442+
application/json:
443+
schema:
444+
$ref: "#/components/schemas/DeepNestedDiscriminatorBase"
445+
420446
components:
421447
schemas:
422448
BaseBasic:
@@ -664,6 +690,57 @@ components:
664690
variant-a: "#/components/schemas/NestedVariantA"
665691
variant-b: "#/components/schemas/NestedVariantB"
666692

693+
DeepNestedDiscriminatorBase:
694+
allOf:
695+
- $ref: "#/components/schemas/CommonProps"
696+
- allOf:
697+
- $ref: "#/components/schemas/DeepLayerCommon"
698+
- oneOf:
699+
- $ref: "#/components/schemas/DeepNestedVariantA"
700+
- $ref: "#/components/schemas/DeepNestedVariantB"
701+
discriminator:
702+
propertyName: deepVariantType
703+
mapping:
704+
deep-a: "#/components/schemas/DeepNestedVariantA"
705+
deep-b: "#/components/schemas/DeepNestedVariantB"
706+
707+
DeepLayerCommon:
708+
type: object
709+
properties:
710+
layer:
711+
type: string
712+
enum: ["inner"]
713+
required:
714+
- layer
715+
716+
DeepNestedVariantA:
717+
type: object
718+
properties:
719+
deepVariantType:
720+
type: string
721+
enum: ["deep-a"]
722+
deepAConfig:
723+
type: object
724+
properties:
725+
enabled:
726+
type: boolean
727+
required:
728+
- deepVariantType
729+
730+
DeepNestedVariantB:
731+
type: object
732+
properties:
733+
deepVariantType:
734+
type: string
735+
enum: ["deep-b"]
736+
deepBConfig:
737+
type: object
738+
properties:
739+
threshold:
740+
type: number
741+
required:
742+
- deepVariantType
743+
667744
NestedVariantA:
668745
type: object
669746
properties:

packages/docusaurus-theme-openapi-docs/src/theme/Schema/index.tsx

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,39 @@ const findProperty = (
8585
return undefined;
8686
};
8787

88+
/**
89+
* Recursively searches for a discriminator in a schema, including nested
90+
* oneOf, anyOf, and allOf structures.
91+
*/
92+
const findDiscriminator = (schema: SchemaObject): any | undefined => {
93+
if (schema.discriminator) {
94+
return schema.discriminator;
95+
}
96+
97+
if (schema.oneOf) {
98+
for (const subschema of schema.oneOf) {
99+
const found = findDiscriminator(subschema as SchemaObject);
100+
if (found) return found;
101+
}
102+
}
103+
104+
if (schema.anyOf) {
105+
for (const subschema of schema.anyOf) {
106+
const found = findDiscriminator(subschema as SchemaObject);
107+
if (found) return found;
108+
}
109+
}
110+
111+
if (schema.allOf) {
112+
for (const subschema of schema.allOf) {
113+
const found = findDiscriminator(subschema as SchemaObject);
114+
if (found) return found;
115+
}
116+
}
117+
118+
return undefined;
119+
};
120+
88121
interface MarkdownProps {
89122
text: string | undefined;
90123
}
@@ -354,10 +387,21 @@ const Properties: React.FC<SchemaProps> = ({
354387
discriminator["mapping"] = inferredMapping;
355388
}
356389
if (Object.keys(schema.properties as {}).length === 0) {
357-
// If properties are empty (e.g., after discriminator property removal in oneOf),
358-
// don't render an empty "object" placeholder - just return nothing
359-
// This prevents confusing empty object displays in discriminated unions
360-
return null;
390+
// Hide placeholder only for discriminator cleanup artifacts; preserve
391+
// empty object rendering for schemas that intentionally define no properties.
392+
if (discriminator) {
393+
return null;
394+
}
395+
return (
396+
<SchemaItem
397+
collapsible={false}
398+
name=""
399+
required={false}
400+
schemaName="object"
401+
qualifierMessage={undefined}
402+
schema={{}}
403+
/>
404+
);
361405
}
362406

363407
return (
@@ -1031,22 +1075,16 @@ const SchemaNode: React.FC<SchemaProps> = ({
10311075
return null;
10321076
}
10331077

1034-
// Merge allOf first if present, so discriminators in nested schemas are properly handled
1035-
// This ensures discriminator property lookups work for schemas like:
1036-
// allOf: [{ $ref: CommonProps }, { oneOf: [...], discriminator: {...} }]
1078+
// Resolve discriminator recursively so nested oneOf/anyOf/allOf compositions
1079+
// can still render discriminator tabs.
10371080
let workingSchema = schema;
1038-
if (schema.allOf && !schema.discriminator) {
1039-
// Check if any allOf item has a discriminator that should be hoisted
1040-
const discriminatorItem = schema.allOf.find(
1041-
(item: any) => item.discriminator
1042-
);
1043-
if (discriminatorItem) {
1044-
workingSchema = mergeAllOf(schema) as SchemaObject;
1045-
// Preserve the discriminator from the nested schema
1046-
if (!workingSchema.discriminator && discriminatorItem.discriminator) {
1047-
workingSchema.discriminator = discriminatorItem.discriminator;
1048-
}
1049-
}
1081+
const resolvedDiscriminator =
1082+
schema.discriminator ?? findDiscriminator(schema);
1083+
if (schema.allOf && !schema.discriminator && resolvedDiscriminator) {
1084+
workingSchema = mergeAllOf(schema) as SchemaObject;
1085+
}
1086+
if (!workingSchema.discriminator && resolvedDiscriminator) {
1087+
workingSchema.discriminator = resolvedDiscriminator;
10501088
}
10511089

10521090
if (workingSchema.discriminator) {

0 commit comments

Comments
 (0)