Repository: pydantic/pydantic

Issue:
JSON schema generation of Discriminated Unions doesn't work with lists
### Checks

* [X] I added a descriptive title to this issue
* [X] I have searched (google, github) for similar issues and couldn't find anything
* [X] I have read and followed [the docs](https://pydantic-docs.helpmanual.io/) and still think this is a bug

<!-- Sorry to sound so draconian, but every second saved replying to issues is time spend improving pydantic :-) -->

# Bug

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
pydantic version: 1.9.0
            pydantic compiled: True
                 install path: C:\Users\Adrien\Documents\GitHub\mft\backend\venv\Lib\site-packages\pydantic
               python version: 3.9.5 (tags/v3.9.5:0a7dcbd, May  3 2021, 17:27:52) [MSC v.1928 64 bit (AMD64)]
                     platform: Windows-10-10.0.19044-SP0
     optional deps. installed: ['dotenv', 'email-validator', 'typing-extensions']
```
<!-- or if you're using pydantic prior to v1.3, manually include: OS, python version and pydantic version -->

<!-- Please read the [docs](https://pydantic-docs.helpmanual.io/) and search through issues to
confirm your bug hasn't already been reported. -->

<!-- Where possible please include a self-contained code snippet describing your bug: -->

I tested the ne feature "discriminated unions", using the sample at https://pydantic-docs.helpmanual.io/usage/types/#discriminated-unions-aka-tagged-unions, and it works great.

But when I use it in arrays, it doesn't work. The resulting json schema doesn't contains any discriminator.

See here, I modified the sample ( pet: Pet to pet: List[Pet] ) :

```py
from typing import Literal, Union, List
from typing_extensions import Annotated
from pydantic import BaseModel, Field

class BlackCat(BaseModel):
    pet_type: Literal['cat']
    color: Literal['black']
    black_name: str

class WhiteCat(BaseModel):
    pet_type: Literal['cat']
    color: Literal['white']
    white_name: str

Cat = Annotated[Union[BlackCat, WhiteCat], Field(discriminator='color')]

class Dog(BaseModel):
    pet_type: Literal['dog']
    name: str

Pet = Annotated[Union[Cat, Dog], Field(discriminator='pet_type')]

class Model(BaseModel):
    pet: List[Pet]
    n: int

print(Model.schema_json())

...
```

JSON schema gives : 
```json
"pet":{
         "title":"Pet",
         "type":"array",
         "items":{
            "anyOf":[
               {
                  "anyOf":[
                     {
                        "$ref":"#/definitions/BlackCat"
                     },
                     {
                        "$ref":"#/definitions/WhiteCat"
                     }
                  ]
               },
               {
                  "$ref":"#/definitions/Dog"
               }
            ]
         }
      }
```



Discussion:
Maybe the title should be more specific. The parsing works correctly AFAICS. The JSON schema generation doesn't.
Hi all, this workaround seems to work for me, until using the Annotation directly is fixed for lists:

```python
from typing import Literal, Annotated, Union
from pydantic import BaseModel, Field


class Dog(BaseModel):
    animal_type: Literal['dog']
    name: str
    claw: str

class Bird(BaseModel):
    animal_type: Literal['bird']
    name: str
    wing_size: str


class Animal(BaseModel):
    __root__: Annotated[
        Union[
            Dog,
            Bird
        ],
        Field(discriminator='animal_type')
    ]


# Animal = Annotated[
#     Union[
#         Dog,
#         Bird
#     ],
#     Field(discriminator='animal_type')
# ]

class Zoo(BaseModel):
    animals: list[Animal]


print(Zoo.schema_json())
```

The schema I get:

```json
{
  "title": "Zoo",
  "type": "object",
  "properties": {
    "animals": {
      "title": "Animals",
      "type": "array",
      "items": {
        "$ref": "#/definitions/Animal"
      }
    }
  },
  "required": [
    "animals"
  ],
  "definitions": {
    "Dog": {
      "title": "Dog",
      "type": "object",
      "properties": {
        "animal_type": {
          "title": "Animal Type",
          "enum": [
            "dog"
          ],
          "type": "string"
        },
        "name": {
          "title": "Name",
          "type": "string"
        },
        "claw": {
          "title": "Claw",
          "type": "string"
        }
      },
      "required": [
        "animal_type",
        "name",
        "claw"
      ]
    },
    "Bird": {
      "title": "Bird",
      "type": "object",
      "properties": {
        "animal_type": {
          "title": "Animal Type",
          "enum": [
            "bird"
          ],
          "type": "string"
        },
        "name": {
          "title": "Name",
          "type": "string"
        },
        "wing_size": {
          "title": "Wing Size",
          "type": "string"
        }
      },
      "required": [
        "animal_type",
        "name",
        "wing_size"
      ]
    },
    "Animal": {
      "title": "Animal",
      "discriminator": {
        "propertyName": "animal_type",
        "mapping": {
          "dog": "#/definitions/Dog",
          "bird": "#/definitions/Bird"
        }
      },
      "anyOf": [
        {
          "$ref": "#/definitions/Dog"
        },
        {
          "$ref": "#/definitions/Bird"
        }
      ]
    }
  }
}
```
@nayef-livio-derwiche thank, it worked for me !

I just had to add this to the Animal class, to be able to use dot notation for getting attributes: 

```python
    def __getattr__(self, item):  # if you want to use '.'
        return self.__root__.__getattribute__(item)
```
@adrienyhuel 

Sure, something like that might help if you want to use the root class transparently:

```python
def use_root_class_decorator(cls):
    def new_method_proxy(func):
        def inner(self, *args):
            return func(self.__root__, *args)
        return inner
    
    cls.__getattr__ = new_method_proxy(getattr)
    cls.__setattr__ = new_method_proxy(setattr)
    cls.__delattr__ = new_method_proxy(delattr)
    cls.__getattr__ = new_method_proxy(getattr)
    cls.__bytes__ = new_method_proxy(bytes)
    cls.__str__ = new_method_proxy(str)
    cls.__bool__ = new_method_proxy(bool)
    cls.__dir__ = new_method_proxy(dir)
    cls.__hash__ = new_method_proxy(hash)
    # not sure of the consequences of using / not using the following line...
    # cls.__class__ = property(new_method_proxy(operator.attrgetter("__class__")))
    cls.__eq__ = new_method_proxy(operator.eq)
    cls.__lt__ = new_method_proxy(operator.lt)
    cls.__gt__ = new_method_proxy(operator.gt)
    cls.__ne__ = new_method_proxy(operator.ne)
    cls.__hash__ = new_method_proxy(hash)
    cls.__getitem__ = new_method_proxy(operator.getitem)
    cls.__setitem__ = new_method_proxy(operator.setitem)
    cls.__delitem__ = new_method_proxy(operator.delitem)
    cls.__iter__ = new_method_proxy(iter)
    cls.__len__ = new_method_proxy(len)
    cls.__contains__ = new_method_proxy(operator.contains)
    
    return cls

@use_root_class_decorator
class Animal(BaseModel):
    __root__: Annotated[
        Union[
            Dog,
            Bird
        ],
        Field(discriminator='animal_type')
    ]
```

Or if you can, you may use this lib: https://github.com/GrahamDumpleton/wrapt



Resolving patch:
diff --git a/pydantic/schema.py b/pydantic/schema.py
--- a/pydantic/schema.py
+++ b/pydantic/schema.py
@@ -265,36 +265,6 @@ def field_schema(
         known_models=known_models or set(),
     )
 
-    # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#discriminator-object
-    if field.discriminator_key is not None:
-        assert field.sub_fields_mapping is not None
-
-        discriminator_models_refs: Dict[str, Union[str, Dict[str, Any]]] = {}
-
-        for discriminator_value, sub_field in field.sub_fields_mapping.items():
-            # sub_field is either a `BaseModel` or directly an `Annotated` `Union` of many
-            if is_union(get_origin(sub_field.type_)):
-                sub_models = get_sub_types(sub_field.type_)
-                discriminator_models_refs[discriminator_value] = {
-                    model_name_map[sub_model]: get_schema_ref(
-                        model_name_map[sub_model], ref_prefix, ref_template, False
-                    )
-                    for sub_model in sub_models
-                }
-            else:
-                sub_field_type = sub_field.type_
-                if hasattr(sub_field_type, '__pydantic_model__'):
-                    sub_field_type = sub_field_type.__pydantic_model__
-
-                discriminator_model_name = model_name_map[sub_field_type]
-                discriminator_model_ref = get_schema_ref(discriminator_model_name, ref_prefix, ref_template, False)
-                discriminator_models_refs[discriminator_value] = discriminator_model_ref['$ref']
-
-        s['discriminator'] = {
-            'propertyName': field.discriminator_alias,
-            'mapping': discriminator_models_refs,
-        }
-
     # $ref will only be returned when there are no schema_overrides
     if '$ref' in f_schema:
         return f_schema, f_definitions, f_nested_models
@@ -718,7 +688,7 @@ def enum_process_schema(enum: Type[Enum], *, field: Optional[ModelField] = None)
 
 
 def field_singleton_sub_fields_schema(
-    sub_fields: Sequence[ModelField],
+    field: ModelField,
     *,
     by_alias: bool,
     model_name_map: Dict[TypeModelOrEnum, str],
@@ -733,6 +703,7 @@ def field_singleton_sub_fields_schema(
     Take a list of Pydantic ``ModelField`` from the declaration of a type with parameters, and generate their
     schema. I.e., fields used as "type parameters", like ``str`` and ``int`` in ``Tuple[str, int]``.
     """
+    sub_fields = cast(List[ModelField], field.sub_fields)
     definitions = {}
     nested_models: Set[str] = set()
     if len(sub_fields) == 1:
@@ -746,6 +717,37 @@ def field_singleton_sub_fields_schema(
             known_models=known_models,
         )
     else:
+        s: Dict[str, Any] = {}
+        # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#discriminator-object
+        if field.discriminator_key is not None:
+            assert field.sub_fields_mapping is not None
+
+            discriminator_models_refs: Dict[str, Union[str, Dict[str, Any]]] = {}
+
+            for discriminator_value, sub_field in field.sub_fields_mapping.items():
+                # sub_field is either a `BaseModel` or directly an `Annotated` `Union` of many
+                if is_union(get_origin(sub_field.type_)):
+                    sub_models = get_sub_types(sub_field.type_)
+                    discriminator_models_refs[discriminator_value] = {
+                        model_name_map[sub_model]: get_schema_ref(
+                            model_name_map[sub_model], ref_prefix, ref_template, False
+                        )
+                        for sub_model in sub_models
+                    }
+                else:
+                    sub_field_type = sub_field.type_
+                    if hasattr(sub_field_type, '__pydantic_model__'):
+                        sub_field_type = sub_field_type.__pydantic_model__
+
+                    discriminator_model_name = model_name_map[sub_field_type]
+                    discriminator_model_ref = get_schema_ref(discriminator_model_name, ref_prefix, ref_template, False)
+                    discriminator_models_refs[discriminator_value] = discriminator_model_ref['$ref']
+
+            s['discriminator'] = {
+                'propertyName': field.discriminator_alias,
+                'mapping': discriminator_models_refs,
+            }
+
         sub_field_schemas = []
         for sf in sub_fields:
             sub_schema, sub_definitions, sub_nested_models = field_type_schema(
@@ -763,9 +765,14 @@ def field_singleton_sub_fields_schema(
                 # object. Otherwise we will end up with several allOf inside anyOf.
                 # See https://github.com/samuelcolvin/pydantic/issues/1209
                 sub_schema = sub_schema['allOf'][0]
+
+            if sub_schema.keys() == {'discriminator', 'anyOf'}:
+                # we don't want discriminator information inside anyOf choices, this is dealt with elsewhere
+                sub_schema.pop('discriminator')
             sub_field_schemas.append(sub_schema)
             nested_models.update(sub_nested_models)
-        return {'anyOf': sub_field_schemas}, definitions, nested_models
+        s['anyOf'] = sub_field_schemas
+        return s, definitions, nested_models
 
 
 # Order is important, e.g. subclasses of str must go before str
@@ -849,7 +856,7 @@ def field_singleton_schema(  # noqa: C901 (ignore complexity)
         (field.field_info and field.field_info.const) or not lenient_issubclass(field_type, BaseModel)
     ):
         return field_singleton_sub_fields_schema(
-            field.sub_fields,
+            field,
             by_alias=by_alias,
             model_name_map=model_name_map,
             schema_overrides=schema_overrides,


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -2915,3 +2915,89 @@ class NestedModel:
             }
         },
     }
+
+
+def test_discriminated_union_in_list():
+    class BlackCat(BaseModel):
+        pet_type: Literal['cat']
+        color: Literal['black']
+        black_name: str
+
+    class WhiteCat(BaseModel):
+        pet_type: Literal['cat']
+        color: Literal['white']
+        white_name: str
+
+    Cat = Annotated[Union[BlackCat, WhiteCat], Field(discriminator='color')]
+
+    class Dog(BaseModel):
+        pet_type: Literal['dog']
+        name: str
+
+    Pet = Annotated[Union[Cat, Dog], Field(discriminator='pet_type')]
+
+    class Model(BaseModel):
+        pets: Pet
+        n: int
+
+    assert Model.schema() == {
+        'title': 'Model',
+        'type': 'object',
+        'properties': {
+            'pets': {
+                'title': 'Pets',
+                'discriminator': {
+                    'propertyName': 'pet_type',
+                    'mapping': {
+                        'cat': {
+                            'BlackCat': {'$ref': '#/definitions/BlackCat'},
+                            'WhiteCat': {'$ref': '#/definitions/WhiteCat'},
+                        },
+                        'dog': '#/definitions/Dog',
+                    },
+                },
+                'anyOf': [
+                    {
+                        'anyOf': [
+                            {'$ref': '#/definitions/BlackCat'},
+                            {'$ref': '#/definitions/WhiteCat'},
+                        ],
+                    },
+                    {'$ref': '#/definitions/Dog'},
+                ],
+            },
+            'n': {'title': 'N', 'type': 'integer'},
+        },
+        'required': ['pets', 'n'],
+        'definitions': {
+            'BlackCat': {
+                'title': 'BlackCat',
+                'type': 'object',
+                'properties': {
+                    'pet_type': {'title': 'Pet Type', 'enum': ['cat'], 'type': 'string'},
+                    'color': {'title': 'Color', 'enum': ['black'], 'type': 'string'},
+                    'black_name': {'title': 'Black Name', 'type': 'string'},
+                },
+                'required': ['pet_type', 'color', 'black_name'],
+            },
+            'WhiteCat': {
+                'title': 'WhiteCat',
+                'type': 'object',
+                'properties': {
+                    'pet_type': {'title': 'Pet Type', 'enum': ['cat'], 'type': 'string'},
+                    'color': {'title': 'Color', 'enum': ['white'], 'type': 'string'},
+                    'white_name': {'title': 'White Name', 'type': 'string'},
+                },
+                'required': ['pet_type', 'color', 'white_name'],
+            },
+            'Dog': {
+                'title': 'Dog',
+                'type': 'object',
+                'properties': {
+                    'pet_type': {'title': 'Pet Type', 'enum': ['dog'], 'type': 'string'},
+                    'name': {'title': 'Name', 'type': 'string'},
+                },
+                'required': ['pet_type', 'name'],
+            },
+        },
+    }