diff --git a/.github/pr-screenshots/926/asyncapi-union-variants.png b/.github/pr-screenshots/926/asyncapi-union-variants.png
new file mode 100644
index 000000000..27504b68f
Binary files /dev/null and b/.github/pr-screenshots/926/asyncapi-union-variants.png differ
diff --git a/docs-main/reference/json-api-asyncapi-reference/operations/v2-commands-completions/subscribe.mdx b/docs-main/reference/json-api-asyncapi-reference/operations/v2-commands-completions/subscribe.mdx
index 03eedc353..980aaf8ca 100644
--- a/docs-main/reference/json-api-asyncapi-reference/operations/v2-commands-completions/subscribe.mdx
+++ b/docs-main/reference/json-api-asyncapi-reference/operations/v2-commands-completions/subscribe.mdx
@@ -131,6 +131,18 @@ title: "Subscribe completions"
+
+
Variants
+
+
+
+
CompletionStreamResponse
+
+
object
+
+
+
+
@@ -144,6 +156,190 @@ title: "Subscribe completions"
+
+
Variants
+
+
+
+
completionResponse
+
+
oneOf
+
+
+
+
+
+
+
+
+ Completion
+ object
+
+
+
+
+
+
+
+
+
Variants
+
+
+
+
Completion
+
+
object
+
+
+
+
+
+
+
+
+ Completion
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Empty
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
OffsetCheckpoint
+
+
object
+
+
+
+
+
+
+
+
+ OffsetCheckpoint
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
JsCantonError
+
+
object
+
+
+
+
+
+
+
+
+ code
+ string
+
+ required
+
+
+
+
+
+
+
+ cause
+ string
+
+ required
+
+
+
+
+
+
+
+ context
+ object
+
+ required
+
+
+
+
+
+
+
+ errorCategory
+ string
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
@@ -175,6 +371,18 @@ title: "Subscribe completions"
+
+
Variants
+
+
+
+
CompletionStreamResponse
+
+
object
+
+
+
+
@@ -188,6 +396,190 @@ title: "Subscribe completions"
+
+
Variants
+
+
+
+
completionResponse
+
+
oneOf
+
+
+
+
+
+
+
+
+ Completion
+ object
+
+
+
+
+
+
+
+
+
Variants
+
+
+
+
Completion
+
+
object
+
+
+
+
+
+
+
+
+ Completion
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Empty
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
OffsetCheckpoint
+
+
object
+
+
+
+
+
+
+
+
+ OffsetCheckpoint
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
JsCantonError
+
+
object
+
+
+
+
+
+
+
+
+ code
+ string
+
+ required
+
+
+
+
+
+
+
+ cause
+ string
+
+ required
+
+
+
+
+
+
+
+ context
+ object
+
+ required
+
+
+
+
+
+
+
+ errorCategory
+ string
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs-main/reference/json-api-asyncapi-reference/operations/v2-state-active-contracts/subscribe.mdx b/docs-main/reference/json-api-asyncapi-reference/operations/v2-state-active-contracts/subscribe.mdx
index 9a6103b4d..7cae38995 100644
--- a/docs-main/reference/json-api-asyncapi-reference/operations/v2-state-active-contracts/subscribe.mdx
+++ b/docs-main/reference/json-api-asyncapi-reference/operations/v2-state-active-contracts/subscribe.mdx
@@ -131,6 +131,18 @@ title: "Subscribe active contracts"
+
+
Variants
+
+
+
+
JsCantonError
+
+
object
+
+
+
+
@@ -138,6 +150,8 @@ title: "Subscribe active contracts"
code
string
+ required
+
@@ -147,6 +161,8 @@ title: "Subscribe active contracts"
cause
string
+
required
+
@@ -156,6 +172,8 @@ title: "Subscribe active contracts"
context
object
+
required
+
@@ -165,6 +183,132 @@ title: "Subscribe active contracts"
errorCategory
string
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
JsGetActiveContractsResponse
+
+
object
+
+
+
+
+
+
+
+
+ workflowId
+ string
+
+
+
+
+
+
+
+ contractEntry
+ object
+
+
+
+
+
+
+
+ streamContinuationToken
+ string
+
+
+
+
+
+
+
+
+
Variants
+
+
+
+
contractEntry
+
+
oneOf
+
+
+
+
+
+
+
+
+ JsActiveContract
+ object
+
+
+
+
+
+
+
+
+
Variants
+
+
+
+
JsActiveContract
+
+
object
+
+
+
+
+
+
+
+
+ JsActiveContract
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ JsEmpty
+ object
+
+ required
+
@@ -173,6 +317,77 @@ title: "Subscribe active contracts"
+
+
+
+
+
JsIncompleteAssigned
+
+
object
+
+
+
+
+
+
+
+
+ JsIncompleteAssigned
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
JsIncompleteUnassigned
+
+
object
+
+
+
+
+
+
+
+
+ JsIncompleteUnassigned
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -202,6 +417,18 @@ title: "Subscribe active contracts"
+
+
Variants
+
+
+
+
JsCantonError
+
+
object
+
+
+
+
@@ -209,6 +436,8 @@ title: "Subscribe active contracts"
code
string
+ required
+
@@ -218,6 +447,8 @@ title: "Subscribe active contracts"
cause
string
+
required
+
@@ -227,6 +458,8 @@ title: "Subscribe active contracts"
context
object
+
required
+
@@ -236,6 +469,132 @@ title: "Subscribe active contracts"
errorCategory
string
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
JsGetActiveContractsResponse
+
+
object
+
+
+
+
+
+
+
+
+ workflowId
+ string
+
+
+
+
+
+
+
+ contractEntry
+ object
+
+
+
+
+
+
+
+ streamContinuationToken
+ string
+
+
+
+
+
+
+
+
+
Variants
+
+
+
+
contractEntry
+
+
oneOf
+
+
+
+
+
+
+
+
+ JsActiveContract
+ object
+
+
+
+
+
+
+
+
+
Variants
+
+
+
+
JsActiveContract
+
+
object
+
+
+
+
+
+
+
+
+ JsActiveContract
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ JsEmpty
+ object
+
+ required
+
@@ -244,6 +603,77 @@ title: "Subscribe active contracts"
+
+
+
+
+
JsIncompleteAssigned
+
+
object
+
+
+
+
+
+
+
+
+ JsIncompleteAssigned
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
JsIncompleteUnassigned
+
+
object
+
+
+
+
+
+
+
+
+ JsIncompleteUnassigned
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs-main/reference/json-api-asyncapi-reference/operations/v2-updates-flats/subscribe.mdx b/docs-main/reference/json-api-asyncapi-reference/operations/v2-updates-flats/subscribe.mdx
index 5c7b7c409..ef9e109d4 100644
--- a/docs-main/reference/json-api-asyncapi-reference/operations/v2-updates-flats/subscribe.mdx
+++ b/docs-main/reference/json-api-asyncapi-reference/operations/v2-updates-flats/subscribe.mdx
@@ -131,6 +131,18 @@ title: "Subscribe flats"
+
+
Variants
+
+
+
+
JsCantonError
+
+
object
+
+
+
+
@@ -138,6 +150,8 @@ title: "Subscribe flats"
code
string
+ required
+
@@ -147,6 +161,8 @@ title: "Subscribe flats"
cause
string
+
required
+
@@ -156,6 +172,8 @@ title: "Subscribe flats"
context
object
+ required
+
@@ -165,6 +183,142 @@ title: "Subscribe flats"
errorCategory
string
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
JsGetUpdatesResponse
+
+
object
+
+
+
+
+
+
+
+
+ update
+ object
+
+
+
+
+
+
+
+
+
Variants
+
+
+
+
+
+
+
+
+
+ OffsetCheckpoint
+ object
+
+
+
+
+
+
+
+
+
Variants
+
+
+
+
OffsetCheckpoint
+
+
object
+
+
+
+
+
+
+
+
+ OffsetCheckpoint
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Reassignment
+
+
object
+
+
+
+
+
+
+
+
+ Reassignment
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
TopologyTransaction
+
+
object
+
+
+
+
+
+
+
+
+ TopologyTransaction
+ object
+
+ required
+
@@ -173,6 +327,49 @@ title: "Subscribe flats"
+
+
+
+
+
Transaction
+
+
object
+
+
+
+
+
+
+
+
+ Transaction
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -202,6 +399,18 @@ title: "Subscribe flats"
+
+
Variants
+
+
+
+
JsCantonError
+
+
object
+
+
+
+
@@ -209,6 +418,8 @@ title: "Subscribe flats"
code
string
+ required
+
@@ -218,6 +429,8 @@ title: "Subscribe flats"
cause
string
+
required
+
@@ -227,6 +440,8 @@ title: "Subscribe flats"
context
object
+
required
+
@@ -236,6 +451,142 @@ title: "Subscribe flats"
errorCategory
string
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
JsGetUpdatesResponse
+
+
object
+
+
+
+
+
+
+
+
+ update
+ object
+
+
+
+
+
+
+
+
+
Variants
+
+
+
+
+
+
+
+
+
+ OffsetCheckpoint
+ object
+
+
+
+
+
+
+
+
+
Variants
+
+
+
+
OffsetCheckpoint
+
+
object
+
+
+
+
+
+
+
+
+ OffsetCheckpoint
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Reassignment
+
+
object
+
+
+
+
+
+
+
+
+ Reassignment
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
TopologyTransaction
+
+
object
+
+
+
+
+
+
+
+
+ TopologyTransaction
+ object
+
+ required
+
@@ -244,6 +595,49 @@ title: "Subscribe flats"
+
+
+
+
+
Transaction
+
+
object
+
+
+
+
+
+
+
+
+ Transaction
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs-main/reference/json-api-asyncapi-reference/operations/v2-updates-trees/subscribe.mdx b/docs-main/reference/json-api-asyncapi-reference/operations/v2-updates-trees/subscribe.mdx
index c9c889e4e..1dfa0446b 100644
--- a/docs-main/reference/json-api-asyncapi-reference/operations/v2-updates-trees/subscribe.mdx
+++ b/docs-main/reference/json-api-asyncapi-reference/operations/v2-updates-trees/subscribe.mdx
@@ -131,6 +131,18 @@ title: "Subscribe trees"
+
+
Variants
+
+
+
+
JsCantonError
+
+
object
+
+
+
+
@@ -138,6 +150,8 @@ title: "Subscribe trees"
code
string
+ required
+
@@ -147,6 +161,8 @@ title: "Subscribe trees"
cause
string
+
required
+
@@ -156,6 +172,8 @@ title: "Subscribe trees"
context
object
+ required
+
@@ -165,6 +183,8 @@ title: "Subscribe trees"
errorCategory
string
+ required
+
@@ -173,6 +193,155 @@ title: "Subscribe trees"
+
+
+
+
+
JsGetUpdateTreesResponse
+
+
object
+
+
+
+
+
+
+
+
+ update
+ object
+
+
+
+
+
+
+
+
+
Variants
+
+
+
+
+
+
+
+
+
+ OffsetCheckpoint
+ object
+
+
+
+
+
+
+
+
+
Variants
+
+
+
+
OffsetCheckpoint
+
+
object
+
+
+
+
+
+
+
+
+ OffsetCheckpoint
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Reassignment
+
+
object
+
+
+
+
+
+
+
+
+ Reassignment
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
TransactionTree
+
+
object
+
+
+
+
+
+
+
+
+ TransactionTree
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -202,6 +371,18 @@ title: "Subscribe trees"
+
+
Variants
+
+
+
+
JsCantonError
+
+
object
+
+
+
+
@@ -209,6 +390,8 @@ title: "Subscribe trees"
code
string
+ required
+
@@ -218,6 +401,8 @@ title: "Subscribe trees"
cause
string
+
required
+
@@ -227,6 +412,8 @@ title: "Subscribe trees"
context
object
+
required
+
@@ -236,6 +423,8 @@ title: "Subscribe trees"
errorCategory
string
+ required
+
@@ -244,6 +433,155 @@ title: "Subscribe trees"
+
+
+
+
+
JsGetUpdateTreesResponse
+
+
object
+
+
+
+
+
+
+
+
+ update
+ object
+
+
+
+
+
+
+
+
+
Variants
+
+
+
+
+
+
+
+
+
+ OffsetCheckpoint
+ object
+
+
+
+
+
+
+
+
+
Variants
+
+
+
+
OffsetCheckpoint
+
+
object
+
+
+
+
+
+
+
+
+ OffsetCheckpoint
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Reassignment
+
+
object
+
+
+
+
+
+
+
+
+ Reassignment
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
TransactionTree
+
+
object
+
+
+
+
+
+
+
+
+ TransactionTree
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs-main/reference/json-api-asyncapi-reference/operations/v2-updates/subscribe.mdx b/docs-main/reference/json-api-asyncapi-reference/operations/v2-updates/subscribe.mdx
index 368afad16..a49ae0d0d 100644
--- a/docs-main/reference/json-api-asyncapi-reference/operations/v2-updates/subscribe.mdx
+++ b/docs-main/reference/json-api-asyncapi-reference/operations/v2-updates/subscribe.mdx
@@ -131,6 +131,18 @@ title: "Subscribe updates"
+
+
Variants
+
+
+
+
JsCantonError
+
+
object
+
+
+
+
@@ -138,6 +150,8 @@ title: "Subscribe updates"
code
string
+ required
+
@@ -147,6 +161,8 @@ title: "Subscribe updates"
cause
string
+
required
+
@@ -156,6 +172,8 @@ title: "Subscribe updates"
context
object
+ required
+
@@ -165,6 +183,142 @@ title: "Subscribe updates"
errorCategory
string
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
JsGetUpdatesResponse
+
+
object
+
+
+
+
+
+
+
+
+ update
+ object
+
+
+
+
+
+
+
+
+
Variants
+
+
+
+
+
+
+
+
+
+ OffsetCheckpoint
+ object
+
+
+
+
+
+
+
+
+
Variants
+
+
+
+
OffsetCheckpoint
+
+
object
+
+
+
+
+
+
+
+
+ OffsetCheckpoint
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Reassignment
+
+
object
+
+
+
+
+
+
+
+
+ Reassignment
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
TopologyTransaction
+
+
object
+
+
+
+
+
+
+
+
+ TopologyTransaction
+ object
+
+ required
+
@@ -173,6 +327,49 @@ title: "Subscribe updates"
+
+
+
+
+
Transaction
+
+
object
+
+
+
+
+
+
+
+
+ Transaction
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -202,6 +399,18 @@ title: "Subscribe updates"
+
+
Variants
+
+
+
+
JsCantonError
+
+
object
+
+
+
+
@@ -209,6 +418,8 @@ title: "Subscribe updates"
code
string
+ required
+
@@ -218,6 +429,8 @@ title: "Subscribe updates"
cause
string
+
required
+
@@ -227,6 +440,8 @@ title: "Subscribe updates"
context
object
+
required
+
@@ -236,6 +451,142 @@ title: "Subscribe updates"
errorCategory
string
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
JsGetUpdatesResponse
+
+
object
+
+
+
+
+
+
+
+
+ update
+ object
+
+
+
+
+
+
+
+
+
Variants
+
+
+
+
+
+
+
+
+
+ OffsetCheckpoint
+ object
+
+
+
+
+
+
+
+
+
Variants
+
+
+
+
OffsetCheckpoint
+
+
object
+
+
+
+
+
+
+
+
+ OffsetCheckpoint
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Reassignment
+
+
object
+
+
+
+
+
+
+
+
+ Reassignment
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
TopologyTransaction
+
+
object
+
+
+
+
+
+
+
+
+ TopologyTransaction
+ object
+
+ required
+
@@ -244,6 +595,49 @@ title: "Subscribe updates"
+
+
+
+
+
Transaction
+
+
object
+
+
+
+
+
+
+
+
+ Transaction
+ object
+
+ required
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs-main/styles.css b/docs-main/styles.css
index e174f4d38..12f9adb0f 100644
--- a/docs-main/styles.css
+++ b/docs-main/styles.css
@@ -970,6 +970,47 @@ body:has(.x2mdx-ref-page--operation) [aria-label="Table of contents"] {
overflow-wrap: anywhere;
}
+.x2mdx-ref-schema-variants {
+ display: grid;
+ gap: 0.8rem;
+ margin-top: 0.9rem;
+}
+
+.x2mdx-ref-schema-variant-label {
+ font-size: 0.77rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ color: rgb(107, 114, 128);
+}
+
+.x2mdx-ref-schema-variant {
+ display: grid;
+ gap: 0.65rem;
+ min-width: 0;
+ padding: 0.85rem;
+ border: 1px solid rgba(15, 23, 42, 0.08);
+ border-radius: 0.65rem;
+ background: rgba(248, 250, 252, 0.72);
+}
+
+:root.dark .x2mdx-ref-schema-variant,
+[data-theme="dark"] .x2mdx-ref-schema-variant {
+ border-color: rgba(255, 255, 255, 0.08);
+ background: rgba(17, 24, 39, 0.58);
+}
+
+.x2mdx-ref-schema-variant-head {
+ display: grid;
+ gap: 0.25rem;
+}
+
+.x2mdx-ref-schema-variant-head h4 {
+ margin: 0;
+ font-size: 0.94rem;
+ line-height: 1.3;
+ overflow-wrap: anywhere;
+}
+
body:has(.x2mdx-ref-hero) #table-of-contents-content a,
body:has(.x2mdx-ref-hero) [aria-label="Table of contents"] a {
overflow-wrap: anywhere;
diff --git a/src/x2mdx/asyncapi/lifecycle.py b/src/x2mdx/asyncapi/lifecycle.py
index 56763c721..a9e693e6c 100644
--- a/src/x2mdx/asyncapi/lifecycle.py
+++ b/src/x2mdx/asyncapi/lifecycle.py
@@ -17,6 +17,7 @@
AsyncApiChannelLifecycle,
AsyncApiMessageDetail,
AsyncApiReport,
+ AsyncApiSchemaVariantDetail,
AsyncApiSourceSnapshot,
)
from x2mdx.types import JsonObject, JsonValue
@@ -312,6 +313,113 @@ def schema_sample_value(
return schema_type_token(doc, resolved)
+def schema_variant_name(doc: AsyncApiDocument, schema: JsonValue | None, *, index: int) -> str:
+ ref_name = local_ref_name(schema, prefix="components/schemas")
+ if ref_name:
+ return ref_name
+
+ resolved = resolve_local_ref(doc, schema)
+ if not isinstance(resolved, dict):
+ return f"Variant {index + 1}"
+
+ title = resolved.get("title")
+ if isinstance(title, str) and title.strip():
+ return title.strip()
+
+ properties = resolved.get("properties")
+ if isinstance(properties, dict) and len(properties) == 1:
+ return str(next(iter(properties)))
+
+ required = resolved.get("required")
+ if isinstance(required, list) and len(required) == 1:
+ return str(required[0])
+
+ return f"Variant {index + 1}"
+
+
+def schema_variants(
+ doc: AsyncApiDocument,
+ schema: JsonValue | None,
+ *,
+ max_depth: int = REQUEST_SAMPLE_MAX_DEPTH,
+ seen_refs: set[str] | None = None,
+) -> list[AsyncApiSchemaVariantDetail]:
+ if max_depth <= 0:
+ return []
+
+ if seen_refs is None:
+ seen_refs = set()
+
+ if isinstance(schema, dict):
+ ref = schema.get("$ref")
+ if isinstance(ref, str):
+ if ref in seen_refs:
+ return []
+ return schema_variants(
+ doc,
+ resolve_local_ref(doc, schema),
+ max_depth=max_depth,
+ seen_refs=seen_refs | {ref},
+ )
+
+ resolved = resolve_local_ref(doc, schema)
+ if not isinstance(resolved, dict):
+ return []
+
+ raw_variants = resolved.get("oneOf")
+ if not isinstance(raw_variants, list):
+ raw_variants = resolved.get("anyOf")
+ if not isinstance(raw_variants, list):
+ property_variants: list[AsyncApiSchemaVariantDetail] = []
+ properties, required = object_schema_properties_and_required(doc, resolved, max_depth=max_depth)
+ for property_name, property_schema in properties.items():
+ nested_variants = schema_variants(
+ doc,
+ property_schema,
+ max_depth=max_depth - 1,
+ seen_refs=seen_refs,
+ )
+ if nested_variants:
+ property_variants.append(
+ {
+ "name": property_name,
+ "payload_schema": schema_brief(doc, property_schema),
+ "required_fields": [property_name] if property_name in required else [],
+ "sample": schema_sample_value(
+ doc,
+ property_schema,
+ max_depth=max_depth,
+ seen_refs=seen_refs,
+ ),
+ "variants": nested_variants,
+ }
+ )
+ return property_variants
+
+ variants: list[AsyncApiSchemaVariantDetail] = []
+ for index, variant_schema in enumerate(raw_variants):
+ variants.append(
+ {
+ "name": schema_variant_name(doc, variant_schema, index=index),
+ "payload_schema": schema_brief(doc, variant_schema),
+ "required_fields": schema_required_field_names(doc, variant_schema),
+ "sample": schema_sample_value(
+ doc,
+ variant_schema,
+ max_depth=max_depth,
+ seen_refs=seen_refs,
+ ),
+ "variants": schema_variants(
+ doc,
+ variant_schema,
+ max_depth=max_depth - 1,
+ seen_refs=seen_refs,
+ ),
+ }
+ )
+ return variants
+
+
def extract_message_detail(doc: AsyncApiDocument, message_node: JsonValue | None) -> AsyncApiMessageDetail:
resolved_message = resolve_local_ref(doc, message_node)
message_name = local_ref_name(message_node, prefix="components/messages")
@@ -322,6 +430,7 @@ def extract_message_detail(doc: AsyncApiDocument, message_node: JsonValue | None
"payload_schema": "-",
"required_fields": [],
"sample": None,
+ "variants": [],
}
payload = resolved_message.get("payload")
@@ -331,6 +440,7 @@ def extract_message_detail(doc: AsyncApiDocument, message_node: JsonValue | None
"payload_schema": schema_brief(doc, payload) if payload is not None else "-",
"required_fields": schema_required_field_names(doc, payload) if payload is not None else [],
"sample": schema_sample_value(doc, payload) if payload is not None else None,
+ "variants": schema_variants(doc, payload) if payload is not None else [],
}
diff --git a/src/x2mdx/asyncapi/models.py b/src/x2mdx/asyncapi/models.py
index 1da10c837..5fb68faab 100644
--- a/src/x2mdx/asyncapi/models.py
+++ b/src/x2mdx/asyncapi/models.py
@@ -15,12 +15,21 @@ class AsyncApiDocument(TypedDict, total=False):
components: JsonObject
+class AsyncApiSchemaVariantDetail(TypedDict):
+ name: str
+ payload_schema: str
+ required_fields: list[str]
+ sample: JsonValue | None
+ variants: list["AsyncApiSchemaVariantDetail"]
+
+
class AsyncApiMessageDetail(TypedDict):
name: str
content_type: str
payload_schema: str
required_fields: list[str]
sample: JsonValue | None
+ variants: list[AsyncApiSchemaVariantDetail]
class AsyncApiActionDetail(TypedDict):
diff --git a/src/x2mdx/asyncapi/render.py b/src/x2mdx/asyncapi/render.py
index b18ed2d75..6cd6acdfb 100644
--- a/src/x2mdx/asyncapi/render.py
+++ b/src/x2mdx/asyncapi/render.py
@@ -18,6 +18,7 @@
ReferenceMetaItem,
ReferenceOperationPage,
ReferencePanel,
+ ReferenceSchema,
ReferenceSection,
compact_text,
markdown_page_from_template,
@@ -109,8 +110,17 @@ def action_schema(action: dict[str, Any], *, anchor: str):
message = dict(action.get("message") or {})
sample = message.get("sample")
required_fields = list(message.get("required_fields") or [])
- if sample is None and not required_fields:
+ variant_schemas = [schema_from_variant(variant) for variant in message.get("variants") or []]
+ if sample is None and not required_fields and not variant_schemas:
return None
+ if variant_schemas:
+ return ReferenceSchema(
+ name=str(message.get("name") or action["action"]),
+ summary=str(message.get("payload_schema") or "-"),
+ description=str(action.get("description") or ""),
+ anchor=anchor,
+ variants=variant_schemas,
+ )
return schema_from_sample(
name=str(message.get("name") or action["action"]),
sample=sample,
@@ -121,6 +131,16 @@ def action_schema(action: dict[str, Any], *, anchor: str):
)
+def schema_from_variant(variant: dict[str, Any]) -> ReferenceSchema:
+ return schema_from_sample(
+ name=str(variant.get("name") or "Variant"),
+ sample=variant.get("sample"),
+ required_fields=list(variant.get("required_fields") or []),
+ summary=str(variant.get("payload_schema") or "-"),
+ variants=[schema_from_variant(child) for child in variant.get("variants") or []],
+ )
+
+
def wscat_example(action: dict[str, Any]) -> str:
sample = action["message"].get("sample")
if action["action"] == "publish" and sample is not None:
diff --git a/src/x2mdx/reference_pages.py b/src/x2mdx/reference_pages.py
index 920dc90cc..53adbf2fb 100644
--- a/src/x2mdx/reference_pages.py
+++ b/src/x2mdx/reference_pages.py
@@ -55,6 +55,7 @@ class ReferenceSchema:
description: str = ""
anchor: str | None = None
fields: list[ReferenceField] = field(default_factory=list)
+ variants: list["ReferenceSchema"] = field(default_factory=list)
enum_values: list[str] = field(default_factory=list)
example: ReferenceExample | None = None
@@ -233,6 +234,7 @@ def schema_from_sample(
description: str = "",
summary: str = "",
anchor: str | None = None,
+ variants: list[ReferenceSchema] | None = None,
) -> ReferenceSchema:
required = set(required_fields or [])
fields: list[ReferenceField] = []
@@ -261,5 +263,6 @@ def schema_from_sample(
description=description,
anchor=anchor,
fields=fields,
+ variants=variants or [],
example=example,
)
diff --git a/src/x2mdx/templates/shared/reference_macros.md.j2 b/src/x2mdx/templates/shared/reference_macros.md.j2
index 4b17d7377..d8a0f0b6d 100644
--- a/src/x2mdx/templates/shared/reference_macros.md.j2
+++ b/src/x2mdx/templates/shared/reference_macros.md.j2
@@ -145,6 +145,22 @@
{{ escape_mdx_html_text(inline_text(schema.description)) }}
{% endif %}
{{ fields_table(schema.fields) }}
+{%- if schema.variants %}
+
+
Variants
+ {% for variant in schema.variants %}
+
+
+
{{ escape_mdx_html_text(inline_text(variant.name)) }}
+ {% if variant.summary %}
+
{{ escape_mdx_html_text(inline_text(variant.summary)) }}
+ {% endif %}
+
+ {{ schema_body(variant, render_example=render_example, render_description=render_description) }}
+
+ {% endfor %}
+
+{%- endif %}
{% if schema.enum_values %}
{% for value in schema.enum_values %}
diff --git a/tests/test_asyncapi.py b/tests/test_asyncapi.py
index 35321f497..6053b9f51 100644
--- a/tests/test_asyncapi.py
+++ b/tests/test_asyncapi.py
@@ -490,3 +490,88 @@ def test_action_adapter_builds_operation_page_context(self) -> None:
self.assertEqual(operation.outputs[0].schema.name, "-")
self.assertEqual(operation.examples[0].title, "wscat")
self.assertIn("npx wscat -c ", operation.examples[0].body)
+
+ def test_action_adapter_preserves_oneof_response_variants(self) -> None:
+ channel = build_asyncapi_report_from_sources(
+ [
+ self._snapshot(
+ "1.1.0",
+ "published/1.1.0/asyncapi.yaml",
+ """
+ asyncapi: 2.6.0
+ info:
+ title: Sample WebSocket API
+ version: 1.1.0
+ channels:
+ /updates:
+ subscribe:
+ operationId: onUpdates
+ bindings:
+ ws:
+ method: GET
+ message:
+ $ref: '#/components/messages/Either_Error_UpdateResponse'
+ components:
+ schemas:
+ Either_Error_UpdateResponse:
+ title: Either_Error_UpdateResponse
+ oneOf:
+ - $ref: '#/components/schemas/Error'
+ - $ref: '#/components/schemas/UpdateResponse'
+ Error:
+ type: object
+ required: [code]
+ properties:
+ code:
+ type: string
+ UpdateResponse:
+ type: object
+ properties:
+ update:
+ $ref: '#/components/schemas/Update'
+ Update:
+ title: Update
+ oneOf:
+ - type: object
+ required: [Checkpoint]
+ properties:
+ Checkpoint:
+ type: object
+ required: [offset]
+ properties:
+ offset:
+ type: string
+ - type: object
+ required: [Transaction]
+ properties:
+ Transaction:
+ type: object
+ required: [updateId]
+ properties:
+ updateId:
+ type: string
+ messages:
+ Either_Error_UpdateResponse:
+ contentType: application/json
+ payload:
+ $ref: '#/components/schemas/Either_Error_UpdateResponse'
+ """,
+ )
+ ],
+ source_name="unit test fixtures",
+ version_filter="unit test versions",
+ publish_version="1.1.0",
+ ).channels[0]
+
+ action = channel.latest["actions"][0]
+ operation = build_action_operation(channel, action, output_dir=None)
+ schema = operation.outputs[0].schema
+
+ self.assertIsNotNone(schema)
+ assert schema is not None
+ self.assertEqual([variant.name for variant in schema.variants], ["Error", "UpdateResponse"])
+ self.assertEqual(schema.variants[0].fields[0].name, "code")
+ self.assertEqual(schema.variants[1].fields[0].name, "update")
+ nested_groups = schema.variants[1].variants
+ self.assertEqual([variant.name for variant in nested_groups], ["update"])
+ self.assertEqual([variant.name for variant in nested_groups[0].variants], ["Checkpoint", "Transaction"])