Repository: Neoteroi/BlackSheep (python)

Issue:
Application starts fail due OpenAPIHandler can't handle list[UUID4] type in a model
**Describe the bug**
In case a Pydantic model has a field like `list[UUID4]` OpenAPIHandler can't build proper schema for a response with a such model.


```python
app = Application()

docs = OpenAPIHandler(info=Info(title="Example API", version="0.0.1"))
docs.bind_app(app)


class Group(BaseModel):
    users: list[UUID4] = Field(..., title="List of user IDs")


@app.router.post("/")
def get_group() -> Group:
    ...

```

```bash
Traceback (most recent call last):
  File "/home/user/DEV/neoteroi/BlackSheep/blacksheep/server/application.py", line 726, in _handle_lifespan
    await self.start()
  File "/home/user/DEV/neoteroi/BlackSheep/blacksheep/server/application.py", line 715, in start
    await self.after_start.fire()
  File "/home/user/DEV/neoteroi/BlackSheep/blacksheep/server/application.py", line 126, in fire
    await handler(self.context, *args, **kwargs)
  File "/home/user/DEV/neoteroi/BlackSheep/blacksheep/server/openapi/common.py", line 403, in build_docs
    docs = self.generate_documentation(app)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/DEV/neoteroi/BlackSheep/blacksheep/server/openapi/v3.py", line 435, in generate_documentation
    paths = self.get_paths(app)
            ^^^^^^^^^^^^^^^^^^^
  File "/home/user/DEV/neoteroi/BlackSheep/blacksheep/server/openapi/v3.py", line 444, in get_paths
    own_paths = self.get_routes_docs(app.router, path_prefix)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/DEV/neoteroi/BlackSheep/blacksheep/server/openapi/v3.py", line 1131, in get_routes_docs
    responses=self.get_responses(handler) or {},
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/DEV/neoteroi/BlackSheep/blacksheep/server/openapi/v3.py", line 1029, in get_responses
    {
  File "/home/user/DEV/neoteroi/BlackSheep/blacksheep/server/openapi/v3.py", line 1033, in <dictcomp>
    content=self._get_content_from_response_info(value.content),
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/DEV/neoteroi/BlackSheep/blacksheep/server/openapi/v3.py", line 970, in _get_content_from_response_info
    ] = self._get_media_type_from_content_doc(content)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/DEV/neoteroi/BlackSheep/blacksheep/server/openapi/v3.py", line 928, in _get_media_type_from_content_doc
    media_type.schema = self.get_schema_by_type(content_doc.type)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/DEV/neoteroi/BlackSheep/blacksheep/server/openapi/v3.py", line 637, in get_schema_by_type
    schema = self._get_schema_by_type(child_type, type_args)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/DEV/neoteroi/BlackSheep/blacksheep/server/openapi/v3.py", line 653, in _get_schema_by_type
    return self._get_schema_for_class(object_type)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/DEV/neoteroi/BlackSheep/blacksheep/server/openapi/v3.py", line 574, in _get_schema_for_class
    properties[field.name] = self.get_schema_by_type(
                             ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/DEV/neoteroi/BlackSheep/blacksheep/server/openapi/v3.py", line 637, in get_schema_by_type
    schema = self._get_schema_by_type(child_type, type_args)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/DEV/neoteroi/BlackSheep/blacksheep/server/openapi/v3.py", line 660, in _get_schema_by_type
    schema = self._try_get_schema_for_iterable(object_type, type_args)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/DEV/neoteroi/BlackSheep/blacksheep/server/openapi/v3.py", line 723, in _try_get_schema_for_iterable
    items=self.get_schema_by_type(item_type, context_type_args),
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/DEV/neoteroi/BlackSheep/blacksheep/server/openapi/v3.py", line 637, in get_schema_by_type
    schema = self._get_schema_by_type(child_type, type_args)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/DEV/neoteroi/BlackSheep/blacksheep/server/openapi/v3.py", line 665, in _get_schema_by_type
    schema = self._try_get_schema_for_generic(object_type, type_args)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/DEV/neoteroi/BlackSheep/blacksheep/server/openapi/v3.py", line 756, in _try_get_schema_for_generic
    parameters = origin.__parameters__
```

Pull request:
Pass annotated origin type to build OpenAPI docs
A possible solution to fix #474 

Patch:
diff --git a/blacksheep/server/openapi/v3.py b/blacksheep/server/openapi/v3.py
index 3ee247c..c38f535 100644
--- a/blacksheep/server/openapi/v3.py
+++ b/blacksheep/server/openapi/v3.py
@@ -1,11 +1,16 @@
 import collections.abc as collections_abc
 import inspect
+import sys
 import warnings
 from abc import ABC, abstractmethod
 from dataclasses import dataclass, fields, is_dataclass
 from datetime import date, datetime
 from enum import Enum, IntEnum
 from typing import Any, Dict, Iterable, List, Optional, Tuple, Type, Union
+
+if sys.version_info >= (3, 9):
+    from typing import _AnnotatedAlias as AnnotatedAlias
+
 from typing import _GenericAlias as GenericAlias
 from typing import get_type_hints
 from uuid import UUID
@@ -649,6 +654,11 @@ class OpenAPIHandler(APIDocsHandler[OpenAPI]):
         if stored_ref:  # pragma: no cover
             return stored_ref
 
+        if sys.version_info >= (3, 9):
+            if isinstance(object_type, AnnotatedAlias):
+                # Replace Annotated object type with the original type
+                object_type = getattr(object_type, "__origin__")
+
         if self._can_handle_class_type(object_type):
             return self._get_schema_for_class(object_type)
 


Test patch:
diff --git a/tests/test_openapi_v3.py b/tests/test_openapi_v3.py
index 53389e6..6d3c294 100644
--- a/tests/test_openapi_v3.py
+++ b/tests/test_openapi_v3.py
@@ -1,3 +1,4 @@
+import sys
 from dataclasses import dataclass
 from datetime import date, datetime
 from enum import IntEnum
@@ -22,7 +23,14 @@ from openapidocs.v3 import (
 )
 from pydantic import VERSION as PYDANTIC_LIB_VERSION
 from pydantic import BaseModel, HttpUrl
-from pydantic.types import NegativeFloat, PositiveInt, condecimal, confloat, conint
+from pydantic.types import (
+    UUID4,
+    NegativeFloat,
+    PositiveInt,
+    condecimal,
+    confloat,
+    conint,
+)
 
 from blacksheep.server.application import Application
 from blacksheep.server.bindings import FromForm
@@ -78,6 +86,9 @@ class PydCat(BaseModel):
     id: int
     name: str
 
+    if sys.version_info >= (3, 9):
+        childs: list[UUID4]
+
 
 class PydPaginatedSetOfCat(BaseModel):
     items: List[PydCat]
@@ -1048,9 +1059,10 @@ async def test_handling_of_pydantic_class_with_generic(
 
     yaml = serializer.to_yaml(docs.generate_documentation(app))
 
-    assert (
-        yaml.strip()
-        == """
+    if sys.version_info >= (3, 9):
+        assert (
+            yaml.strip()
+            == """
 openapi: 3.0.3
 info:
     title: Example
@@ -1073,6 +1085,7 @@ components:
             required:
             - id
             - name
+            - childs
             properties:
                 id:
                     type: integer
@@ -1081,6 +1094,13 @@ components:
                 name:
                     type: string
                     nullable: false
+                childs:
+                    type: array
+                    nullable: false
+                    items:
+                        type: string
+                        format: uuid
+                        nullable: false
         PydPaginatedSetOfCat:
             type: object
             required:
@@ -1098,7 +1118,59 @@ components:
                     nullable: false
 tags: []
 """.strip()
-    )
+        )
+    else:
+        assert (
+            yaml.strip()
+            == """
+openapi: 3.0.3
+info:
+    title: Example
+    version: 0.0.1
+paths:
+    /:
+        get:
+            responses:
+                '200':
+                    description: Success response
+                    content:
+                        application/json:
+                            schema:
+                                $ref: '#/components/schemas/PydPaginatedSetOfCat'
+            operationId: home
+components:
+    schemas:
+        PydCat:
+            type: object
+            required:
+            - id
+            - name
+            properties:
+                id:
+                    type: integer
+                    format: int64
+                    nullable: false
+                name:
+                    type: string
+                    nullable: false
+        PydPaginatedSetOfCat:
+            type: object
+            required:
+            - items
+            - total
+            properties:
+                items:
+                    type: array
+                    nullable: false
+                    items:
+                        $ref: '#/components/schemas/PydCat'
+                total:
+                    type: integer
+                    format: int64
+                    nullable: false
+tags: []
+""".strip()
+        )
 
 
 @pytest.mark.asyncio
@@ -1116,9 +1188,10 @@ async def test_handling_of_pydantic_class_with_child_models(
 
     yaml = serializer.to_yaml(docs.generate_documentation(app))
 
-    assert (
-        yaml.strip()
-        == """
+    if sys.version_info >= (3, 9):
+        assert (
+            yaml.strip()
+            == """
 openapi: 3.0.3
 info:
     title: Example
@@ -1141,6 +1214,7 @@ components:
             required:
             - id
             - name
+            - childs
             properties:
                 id:
                     type: integer
@@ -1149,6 +1223,13 @@ components:
                 name:
                     type: string
                     nullable: false
+                childs:
+                    type: array
+                    nullable: false
+                    items:
+                        type: string
+                        format: uuid
+                        nullable: false
         PydPaginatedSetOfCat:
             type: object
             required:
@@ -1186,8 +1267,81 @@ components:
                 friend:
                     $ref: '#/components/schemas/PydExampleWithSpecificTypes'
 tags: []
-""".strip()
-    )
+    """.strip()
+        )
+    else:
+        assert (
+            yaml.strip()
+            == """
+openapi: 3.0.3
+info:
+    title: Example
+    version: 0.0.1
+paths:
+    /:
+        get:
+            responses:
+                '200':
+                    description: Success response
+                    content:
+                        application/json:
+                            schema:
+                                $ref: '#/components/schemas/PydTypeWithChildModels'
+            operationId: home
+components:
+    schemas:
+        PydCat:
+            type: object
+            required:
+            - id
+            - name
+            properties:
+                id:
+                    type: integer
+                    format: int64
+                    nullable: false
+                name:
+                    type: string
+                    nullable: false
+        PydPaginatedSetOfCat:
+            type: object
+            required:
+            - items
+            - total
+            properties:
+                items:
+                    type: array
+                    nullable: false
+                    items:
+                        $ref: '#/components/schemas/PydCat'
+                total:
+                    type: integer
+                    format: int64
+                    nullable: false
+        PydExampleWithSpecificTypes:
+            type: object
+            required:
+            - url
+            properties:
+                url:
+                    type: string
+                    format: uri
+                    maxLength: 2083
+                    minLength: 1
+                    nullable: false
+        PydTypeWithChildModels:
+            type: object
+            required:
+            - child
+            - friend
+            properties:
+                child:
+                    $ref: '#/components/schemas/PydPaginatedSetOfCat'
+                friend:
+                    $ref: '#/components/schemas/PydExampleWithSpecificTypes'
+tags: []
+    """.strip()
+        )
 
 
 @pytest.mark.asyncio
@@ -1205,9 +1359,10 @@ async def test_handling_of_pydantic_class_in_generic(
 
     yaml = serializer.to_yaml(docs.generate_documentation(app))
 
-    assert (
-        yaml.strip()
-        == """
+    if sys.version_info >= (3, 9):
+        assert (
+            yaml.strip()
+            == """
 openapi: 3.0.3
 info:
     title: Example
@@ -1230,6 +1385,7 @@ components:
             required:
             - id
             - name
+            - childs
             properties:
                 id:
                     type: integer
@@ -1238,6 +1394,13 @@ components:
                 name:
                     type: string
                     nullable: false
+                childs:
+                    type: array
+                    nullable: false
+                    items:
+                        type: string
+                        format: uuid
+                        nullable: false
         PaginatedSetOfPydCat:
             type: object
             required:
@@ -1254,8 +1417,60 @@ components:
                     format: int64
                     nullable: false
 tags: []
-""".strip()
-    )
+    """.strip()
+        )
+    else:
+        assert (
+            yaml.strip()
+            == """
+openapi: 3.0.3
+info:
+    title: Example
+    version: 0.0.1
+paths:
+    /:
+        get:
+            responses:
+                '200':
+                    description: Success response
+                    content:
+                        application/json:
+                            schema:
+                                $ref: '#/components/schemas/PaginatedSetOfPydCat'
+            operationId: home
+components:
+    schemas:
+        PydCat:
+            type: object
+            required:
+            - id
+            - name
+            properties:
+                id:
+                    type: integer
+                    format: int64
+                    nullable: false
+                name:
+                    type: string
+                    nullable: false
+        PaginatedSetOfPydCat:
+            type: object
+            required:
+            - items
+            - total
+            properties:
+                items:
+                    type: array
+                    nullable: false
+                    items:
+                        $ref: '#/components/schemas/PydCat'
+                total:
+                    type: integer
+                    format: int64
+                    nullable: false
+tags: []
+    """.strip()
+        )
 
 
 @pytest.mark.asyncio
@@ -1703,7 +1918,73 @@ async def test_pydantic_generic(docs: OpenAPIHandler, serializer: Serializer):
     yaml = serializer.to_yaml(docs.generate_documentation(app))
 
     if PYDANTIC_VERSION == 1:
-        expected_result = """
+        if sys.version_info >= (3, 9):
+            expected_result = """
+openapi: 3.0.3
+info:
+    title: Example
+    version: 0.0.1
+paths:
+    /:
+        get:
+            responses:
+                '200':
+                    description: Success response
+                    content:
+                        application/json:
+                            schema:
+                                $ref: '#/components/schemas/PydResponse[PydCat]'
+            operationId: home
+components:
+    schemas:
+        PydCat:
+            type: object
+            required:
+            - id
+            - name
+            - childs
+            properties:
+                id:
+                    type: integer
+                    format: int64
+                    nullable: false
+                name:
+                    type: string
+                    nullable: false
+                childs:
+                    type: array
+                    nullable: false
+                    items:
+                        type: string
+                        format: uuid
+                        nullable: false
+        Error:
+            type: object
+            required:
+            - code
+            - message
+            properties:
+                code:
+                    type: integer
+                    format: int64
+                    nullable: false
+                message:
+                    type: string
+                    nullable: false
+        PydResponse[PydCat]:
+            type: object
+            required:
+            - data
+            - error
+            properties:
+                data:
+                    $ref: '#/components/schemas/PydCat'
+                error:
+                    $ref: '#/components/schemas/Error'
+tags: []
+""".strip()
+        else:
+            expected_result = """
 openapi: 3.0.3
 info:
     title: Example
@@ -1760,7 +2041,77 @@ components:
 tags: []
 """.strip()
     elif PYDANTIC_VERSION == 2:
-        expected_result = """
+        if sys.version_info >= (3, 9):
+            expected_result = """
+openapi: 3.0.3
+info:
+    title: Example
+    version: 0.0.1
+paths:
+    /:
+        get:
+            responses:
+                '200':
+                    description: Success response
+                    content:
+                        application/json:
+                            schema:
+                                $ref: '#/components/schemas/PydResponse[PydCat]'
+            operationId: home
+components:
+    schemas:
+        PydCat:
+            type: object
+            required:
+            - id
+            - name
+            - childs
+            properties:
+                id:
+                    type: integer
+                    format: int64
+                    nullable: false
+                name:
+                    type: string
+                    nullable: false
+                childs:
+                    type: array
+                    nullable: false
+                    items:
+                        type: string
+                        format: uuid
+                        nullable: false
+        Error:
+            type: object
+            required:
+            - code
+            - message
+            properties:
+                code:
+                    type: integer
+                    format: int64
+                    nullable: false
+                message:
+                    type: string
+                    nullable: false
+        PydResponse[PydCat]:
+            type: object
+            required:
+            - data
+            - error
+            properties:
+                data:
+                    anyOf:
+                    -   $ref: '#/components/schemas/PydCat'
+                    -   type: 'null'
+                error:
+                    anyOf:
+                    -   $ref: '#/components/schemas/Error'
+                    -   type: 'null'
+tags: []
+""".strip()
+        else:
+            expected_result = """
 openapi: 3.0.3
 info:
     title: Example