Repository: ag2ai/faststream

Issue:
Bug: `AttributeError: 'list' object has no attribute 'items' with "Discriminated Unions"
**Describe the bug**

When using an `Annotated` union with a pydantic `Field(discriminator=...)` AsyncDocs fail to generate due to a `AttributeError: 'list' object has no attribute 'items'`


https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions

```
  File "/Users/kilo59/.local/share/virtualenvs/fs-unions-sN2W4Mxo/lib/python3.11/site-packages/faststream/asyncapi/generate.py", line 186, in _resolve_msg_payloads
    for p_title, p in m.payload.get("oneOf", {}).items():
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

AttributeError: 'list' object has no attribute 'items'
```

**How to reproduce**

```python
# repro.py
from typing import Annotated, Literal

from pydantic import BaseModel
from pydantic.fields import Field
from faststream import FastStream
from faststream.rabbit import RabbitBroker

broker = RabbitBroker("amqp://guest:guest@localhost:5672/")

app = FastStream(broker, version="0.1.0")

class Hotdog(BaseModel):
    type: Literal["hotdog"]
    condiments: list[str]

class Sub(BaseModel):
    type: Literal["sub"]
    condiments: list[str]
    bread: Literal["white", "wheat"]

MyTypeUnion = Annotated[ Hotdog | Sub, Field(discriminator="type")]

@broker.subscriber("test_queue")
async def handle_event(msg_body: MyTypeUnion) -> None:
    print(msg_body)

# Error is thrown when generating docs
# faststream docs gen repro:app
```

**Expected behavior**

I would expect to see docs generated with an appropriate `anyOf` or `oneOf` union.
Note this works with other union types.

```python
# Removing the discriminator from `Field` causes things to work again.
...

MyTypeUnion = Annotated[ Hotdog | Sub, Field(union="smart")]

@broker.subscriber("test_queue")
async def handle_event(msg_body: MyTypeUnion) -> None:
    print(msg_body)
```

**Observed behavior**
Describe what is actually happening clearly and concisely.

**Screenshots**
~If applicable, attach screenshots to help illustrate the problem.~

**Environment**
```
Running FastStream 0.4.4 with CPython 3.11.7 on Darwin
```

**Additional context**
I suspect nesting this field in a higher-level model would also allow me to use the discriminated union but it should be possible without changing the structure of my payload. Especially considering it works for other union types.


Discussion:
This is still an issue. When using unions with discriminator, AsyncAPI doesn't render and blames with error: "discriminator should be string"
@Sesota can you add code snippet to reproduce?



Patch:
diff --git a/faststream/asyncapi/generate.py b/faststream/asyncapi/generate.py
index e46b057804..06f2a603cc 100644
--- a/faststream/asyncapi/generate.py
+++ b/faststream/asyncapi/generate.py
@@ -184,8 +184,6 @@ def _resolve_msg_payloads(
     m.payload = _move_pydantic_refs(m.payload, DEF_KEY)
     if DEF_KEY in m.payload:
         payloads.update(m.payload.pop(DEF_KEY))
-    if "discriminator" in m.payload:
-        m.payload["discriminator"] = m.payload["discriminator"]["propertyName"]
 
     one_of = m.payload.get("oneOf")
     if isinstance(one_of, dict):
@@ -240,4 +238,7 @@ def _move_pydantic_refs(
             for i in range(len(data[k])):
                 data[k][i] = _move_pydantic_refs(item[i], key)
 
+    if isinstance(desciminator := data.get("discriminator"), dict):
+        data["discriminator"] = desciminator["propertyName"]
+
     return data


Test patch:
diff --git a/tests/asyncapi/base/arguments.py b/tests/asyncapi/base/arguments.py
index 16386749d3..d1530f7107 100644
--- a/tests/asyncapi/base/arguments.py
+++ b/tests/asyncapi/base/arguments.py
@@ -26,7 +26,8 @@ def test_custom_naming(self):
         broker = self.broker_class()
 
         @broker.subscriber("test", title="custom_name", description="test description")
-        async def handle(msg): ...
+        async def handle(msg):
+            ...
 
         schema = get_app_schema(self.build_app(broker)).to_jsonable()
         key = tuple(schema["channels"].keys())[0]  # noqa: RUF015
@@ -53,7 +54,8 @@ def test_no_type(self):
         broker = self.broker_class()
 
         @broker.subscriber("test")
-        async def handle(msg): ...
+        async def handle(msg):
+            ...
 
         schema = get_app_schema(self.build_app(broker)).to_jsonable()
 
@@ -67,7 +69,8 @@ def test_simple_type(self):
         broker = self.broker_class()
 
         @broker.subscriber("test")
-        async def handle(msg: int): ...
+        async def handle(msg: int):
+            ...
 
         schema = get_app_schema(self.build_app(broker)).to_jsonable()
 
@@ -82,7 +85,8 @@ def test_simple_optional_type(self):
         broker = self.broker_class()
 
         @broker.subscriber("test")
-        async def handle(msg: Optional[int]): ...
+        async def handle(msg: Optional[int]):
+            ...
 
         schema = get_app_schema(self.build_app(broker)).to_jsonable()
 
@@ -106,7 +110,8 @@ def test_simple_type_with_default(self):
         broker = self.broker_class()
 
         @broker.subscriber("test")
-        async def handle(msg: int = 1): ...
+        async def handle(msg: int = 1):
+            ...
 
         schema = get_app_schema(self.build_app(broker)).to_jsonable()
 
@@ -124,7 +129,8 @@ def test_multi_args_no_type(self):
         broker = self.broker_class()
 
         @broker.subscriber("test")
-        async def handle(msg, another): ...
+        async def handle(msg, another):
+            ...
 
         schema = get_app_schema(self.build_app(broker)).to_jsonable()
 
@@ -146,7 +152,8 @@ def test_multi_args_with_type(self):
         broker = self.broker_class()
 
         @broker.subscriber("test")
-        async def handle(msg: str, another: int): ...
+        async def handle(msg: str, another: int):
+            ...
 
         schema = get_app_schema(self.build_app(broker)).to_jsonable()
 
@@ -168,7 +175,8 @@ def test_multi_args_with_default(self):
         broker = self.broker_class()
 
         @broker.subscriber("test")
-        async def handle(msg: str, another: Optional[int] = None): ...
+        async def handle(msg: str, another: Optional[int] = None):
+            ...
 
         schema = get_app_schema(self.build_app(broker)).to_jsonable()
 
@@ -207,7 +215,8 @@ class User(pydantic.BaseModel):
         broker = self.broker_class()
 
         @broker.subscriber("test")
-        async def handle(user: User): ...
+        async def handle(user: User):
+            ...
 
         schema = get_app_schema(self.build_app(broker)).to_jsonable()
 
@@ -238,7 +247,8 @@ class User(pydantic.BaseModel):
         broker = self.broker_class()
 
         @broker.subscriber("test")
-        async def handle(user: User): ...
+        async def handle(user: User):
+            ...
 
         schema = get_app_schema(self.build_app(broker)).to_jsonable()
 
@@ -276,7 +286,8 @@ class User(pydantic.BaseModel):
         broker = self.broker_class()
 
         @broker.subscriber("test")
-        async def handle(user: User, description: str = ""): ...
+        async def handle(user: User, description: str = ""):
+            ...
 
         schema = get_app_schema(self.build_app(broker)).to_jsonable()
 
@@ -332,7 +343,8 @@ class Config:
         broker = self.broker_class()
 
         @broker.subscriber("test")
-        async def handle(user: User): ...
+        async def handle(user: User):
+            ...
 
         schema = get_app_schema(self.build_app(broker)).to_jsonable()
 
@@ -362,10 +374,12 @@ class User(pydantic.BaseModel):
             "test",
             filter=lambda m: m.content_type == "application/json",
         )
-        async def handle(id: int): ...
+        async def handle(id: int):
+            ...
 
         @broker.subscriber("test")
-        async def handle_default(msg): ...
+        async def handle_default(msg):
+            ...
 
         schema = get_app_schema(self.build_app(broker)).to_jsonable()
 
@@ -396,7 +410,8 @@ def dep2(name2: str):
         message = self.dependency_builder(dep)
 
         @broker.subscriber("test", dependencies=dependencies)
-        async def handle(id: int, message=message): ...
+        async def handle(id: int, message=message):
+            ...
 
         schema = get_app_schema(self.build_app(broker)).to_jsonable()
 
@@ -424,13 +439,14 @@ class Sub(pydantic.BaseModel):
             type: Literal["sub"]
 
         descriminator = Annotated[
-            Union[Sub2, Sub], pydantic.Field(..., discriminator="type")
+            Union[Sub2, Sub], pydantic.Field(discriminator="type")
         ]
 
         broker = self.broker_class()
 
         @broker.subscriber("test")
-        async def handle(user: descriminator): ...
+        async def handle(user: descriminator):
+            ...
 
         schema = get_app_schema(self.build_app(broker)).to_jsonable()
 
@@ -467,6 +483,66 @@ async def handle(user: descriminator): ...
             },
         }, schema["components"]
 
+    @pydanticV2
+    def test_nested_descriminator(self):
+        class Sub2(pydantic.BaseModel):
+            type: Literal["sub2"]
+
+        class Sub(pydantic.BaseModel):
+            type: Literal["sub"]
+
+        class Model(pydantic.BaseModel):
+            msg: Union[Sub2, Sub] = pydantic.Field(..., discriminator="type")
+
+        broker = self.broker_class()
+
+        @broker.subscriber("test")
+        async def handle(user: Model):
+            ...
+
+        schema = get_app_schema(self.build_app(broker)).to_jsonable()
+
+        key = next(iter(schema["components"]["messages"].keys()))
+        assert key == IsStr(regex=r"test[\w:]*:Handle:Message")
+        assert schema["components"] == {
+            "messages": {
+                key: {
+                    "title": key,
+                    "correlationId": {"location": "$message.header#/correlation_id"},
+                    "payload": {"$ref": "#/components/schemas/Model"},
+                }
+            },
+            "schemas": {
+                "Sub": {
+                    "properties": {"type": {"const": "sub", "title": "Type"}},
+                    "required": ["type"],
+                    "title": "Sub",
+                    "type": "object",
+                },
+                "Sub2": {
+                    "properties": {"type": {"const": "sub2", "title": "Type"}},
+                    "required": ["type"],
+                    "title": "Sub2",
+                    "type": "object",
+                },
+                "Model": {
+                    "properties": {
+                        "msg": {
+                            "discriminator": "type",
+                            "oneOf": [
+                                {"$ref": "#/components/schemas/Sub2"},
+                                {"$ref": "#/components/schemas/Sub"},
+                            ],
+                            "title": "Msg",
+                        }
+                    },
+                    "required": ["msg"],
+                    "title": "Model",
+                    "type": "object",
+                },
+            },
+        }, schema["components"]
+
 
 class ArgumentsTestcase(FastAPICompatible):  # noqa: D101
     dependency_builder = staticmethod(Depends)
@@ -482,7 +558,8 @@ async def msg(
                 title="Perfect",
                 examples=[1],
             ),
-        ): ...
+        ):
+            ...
 
         schema = get_app_schema(self.build_app(broker)).to_jsonable()
 
@@ -504,7 +581,8 @@ def test_ignores_custom_field(self):
         broker = self.broker_class()
 
         @broker.subscriber("test")
-        async def handle(id: int, user: Optional[str] = None, message=Context()): ...
+        async def handle(id: int, user: Optional[str] = None, message=Context()):
+            ...
 
         schema = get_app_schema(self.build_app(broker)).to_jsonable()