Skip to content

Commit 2e26316

Browse files
authored
bugfix: nested oneOf/anyOf inside allOf (#1250)
* Fix rendering of allOf with multiple oneOf/anyOf constraints - Detect when allOf contains multiple oneOf/anyOf items - Render each constraint separately instead of merging (which loses information) - Display oneOf/anyOf constraints before shared properties - Fixes issue where nested oneOf schemas were not properly rendered in security-rules schema * Add test case for allOf with multiple oneOf constraints - Demonstrates allOf containing two independent oneOf groups - Shows realistic pattern where object must satisfy multiple constraints - Includes shared properties that apply to all combinations - Tests the fix for rendering multiple oneOf items within allOf
1 parent 583f646 commit 2e26316

File tree

2 files changed

+241
-0
lines changed

2 files changed

+241
-0
lines changed

demo/examples/tests/allOf.yaml

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,205 @@ paths:
412412
required:
413413
- pet
414414

415+
/allof-multiple-oneof:
416+
post:
417+
tags:
418+
- allOf
419+
summary: allOf with multiple oneOf constraints
420+
description: |
421+
Schema demonstrating allOf containing multiple independent oneOf constraints.
422+
The object must satisfy one option from each oneOf group AND include shared properties.
423+
424+
This pattern is used when multiple independent constraints must all be satisfied,
425+
similar to the security-rules schema where a rule must be one policy type AND exist in one scope.
426+
427+
Schema:
428+
```yaml
429+
type: object
430+
allOf:
431+
# Constraint 1: Must be one of these types
432+
- oneOf:
433+
- title: standard
434+
type: object
435+
properties:
436+
type:
437+
type: string
438+
enum: [standard]
439+
priority:
440+
type: integer
441+
minimum: 1
442+
maximum: 10
443+
required: [type, priority]
444+
- title: express
445+
type: object
446+
properties:
447+
type:
448+
type: string
449+
enum: [express]
450+
expedited:
451+
type: boolean
452+
required: [type, expedited]
453+
454+
# Constraint 2: Must exist in one of these scopes
455+
- oneOf:
456+
- title: global
457+
type: object
458+
properties:
459+
scope:
460+
type: string
461+
enum: [global]
462+
required: [scope]
463+
- title: regional
464+
type: object
465+
properties:
466+
scope:
467+
type: string
468+
enum: [regional]
469+
region:
470+
type: string
471+
required: [scope, region]
472+
- title: local
473+
type: object
474+
properties:
475+
scope:
476+
type: string
477+
enum: [local]
478+
location:
479+
type: string
480+
required: [scope, location]
481+
482+
# Shared properties that apply regardless of which options are chosen
483+
properties:
484+
id:
485+
type: string
486+
format: uuid
487+
name:
488+
type: string
489+
description:
490+
type: string
491+
enabled:
492+
type: boolean
493+
default: true
494+
required: [id, name]
495+
```
496+
requestBody:
497+
required: true
498+
content:
499+
application/json:
500+
schema:
501+
type: object
502+
allOf:
503+
# Constraint 1: Must be one of these types
504+
- oneOf:
505+
- title: standard
506+
type: object
507+
properties:
508+
type:
509+
type: string
510+
enum: [standard]
511+
description: Standard processing type
512+
example: standard
513+
priority:
514+
type: integer
515+
minimum: 1
516+
maximum: 10
517+
description: Priority level for standard type
518+
example: 5
519+
required: [type, priority]
520+
- title: express
521+
type: object
522+
properties:
523+
type:
524+
type: string
525+
enum: [express]
526+
description: Express processing type
527+
example: express
528+
expedited:
529+
type: boolean
530+
description: Enable expedited processing
531+
default: true
532+
required: [type, expedited]
533+
534+
# Constraint 2: Must exist in one of these scopes
535+
- oneOf:
536+
- title: global
537+
type: object
538+
properties:
539+
scope:
540+
type: string
541+
enum: [global]
542+
description: Global scope
543+
example: global
544+
required: [scope]
545+
- title: regional
546+
type: object
547+
properties:
548+
scope:
549+
type: string
550+
enum: [regional]
551+
description: Regional scope
552+
example: regional
553+
region:
554+
type: string
555+
pattern: ^[a-z]{2}-[a-z]+-\d+$
556+
description: AWS-style region identifier
557+
example: us-west-2
558+
required: [scope, region]
559+
- title: local
560+
type: object
561+
properties:
562+
scope:
563+
type: string
564+
enum: [local]
565+
description: Local scope
566+
example: local
567+
location:
568+
type: string
569+
maxLength: 100
570+
description: Specific location identifier
571+
example: datacenter-1
572+
required: [scope, location]
573+
574+
# Shared properties
575+
properties:
576+
id:
577+
type: string
578+
format: uuid
579+
description: Unique identifier
580+
example: 123e4567-e89b-12d3-a456-426655440000
581+
name:
582+
type: string
583+
minLength: 1
584+
maxLength: 255
585+
description: Resource name
586+
example: my-resource
587+
description:
588+
type: string
589+
maxLength: 1024
590+
description: Optional description
591+
example: This is a test resource
592+
enabled:
593+
type: boolean
594+
description: Whether the resource is enabled
595+
default: true
596+
required: [id, name]
597+
responses:
598+
"201":
599+
description: Created
600+
content:
601+
application/json:
602+
schema:
603+
type: object
604+
properties:
605+
id:
606+
type: string
607+
format: uuid
608+
name:
609+
type: string
610+
message:
611+
type: string
612+
example: Resource created successfully
613+
415614
components:
416615
schemas:
417616
# Your existing schemas are integrated here.

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,48 @@ const SchemaNode: React.FC<SchemaProps> = ({ schema, schemaType }) => {
885885

886886
// Handle allOf, oneOf, anyOf without discriminators
887887
if (schema.allOf) {
888+
// Check if allOf contains multiple oneOf/anyOf items that should be rendered separately
889+
const oneOfItems = schema.allOf.filter(
890+
(item: any) => item.oneOf || item.anyOf
891+
);
892+
const hasMultipleChoices = oneOfItems.length > 1;
893+
894+
if (hasMultipleChoices) {
895+
// Render each oneOf/anyOf constraint first, then shared properties
896+
const mergedSchemas = mergeAllOf(schema) as SchemaObject;
897+
898+
if (
899+
(schemaType === "request" && mergedSchemas.readOnly) ||
900+
(schemaType === "response" && mergedSchemas.writeOnly)
901+
) {
902+
return null;
903+
}
904+
905+
return (
906+
<div>
907+
{/* Render all oneOf/anyOf constraints first */}
908+
{schema.allOf.map((item: any, index: number) => {
909+
if (item.oneOf || item.anyOf) {
910+
return (
911+
<div key={index}>
912+
<AnyOneOf schema={item} schemaType={schemaType} />
913+
</div>
914+
);
915+
}
916+
return null;
917+
})}
918+
{/* Then render shared properties from the merge */}
919+
{mergedSchemas.properties && (
920+
<Properties schema={mergedSchemas} schemaType={schemaType} />
921+
)}
922+
{mergedSchemas.items && (
923+
<Items schema={mergedSchemas} schemaType={schemaType} />
924+
)}
925+
</div>
926+
);
927+
}
928+
929+
// For other allOf cases, use standard merge behavior
888930
const mergedSchemas = mergeAllOf(schema) as SchemaObject;
889931

890932
if (

0 commit comments

Comments
 (0)