Repository: litestar-org/litestar

Issue:
Bug: OpenAPI schema generation fails for arrays with "oneOf" items
### Description

OpenAPI array schema generation fails when the "items" field contains a "oneOf" schema and more than one element.

### URL to code causing the issue

_No response_

### MCVE

```python
from typing import Tuple
from litestar import get, Litestar


@get("/")
async def route() -> Tuple[str, str | int]:
    return "", 1


app = Litestar(route_handlers=[route])
```


### Steps to reproduce

```bash
1. Run the litestar app in debug mode
2. Send a `GET` request to `/schema/openapi.json`
3. See error
```


### Screenshots

_No response_

### Logs

```bash
❯ litestar run --debug
Using Litestar app from app:app
Starting server process ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
┌──────────────────────────────┬──────────────────────┐
│ Litestar version             │ 2.2.1                │
│ Debug mode                   │ Enabled              │
│ Python Debugger on exception │ Disabled             │
│ CORS                         │ Disabled             │
│ CSRF                         │ Disabled             │
│ OpenAPI                      │ Enabled path=/schema │
│ Compression                  │ Disabled             │
└──────────────────────────────┴──────────────────────┘
INFO:     Started server process [7290]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
ERROR - 2023-10-30 13:28:42,217 - litestar - config - exception raised on http connection to route /schema/openapi.json

Traceback (most recent call last):
  File "**/.venv/lib/python3.10/site-packages/litestar/middleware/exceptions/middleware.py", line 191, in __call__
    await self.app(scope, receive, send)
  File "**/.venv/lib/python3.10/site-packages/litestar/routes/http.py", line 81, in handle
    response = await self._get_response_for_request(
  File "**/.venv/lib/python3.10/site-packages/litestar/routes/http.py", line 133, in _get_response_for_request
    return await self._call_handler_function(
  File "**/.venv/lib/python3.10/site-packages/litestar/routes/http.py", line 153, in _call_handler_function
    response_data, cleanup_group = await self._get_response_data(
  File "**/.venv/lib/python3.10/site-packages/litestar/routes/http.py", line 199, in _get_response_data
    data = route_handler.fn.value(**parsed_kwargs)
  File "**/.venv/lib/python3.10/site-packages/litestar/openapi/controller.py", line 173, in retrieve_schema_json
    body=self._get_schema_as_json(request),
  File "**/.venv/lib/python3.10/site-packages/litestar/openapi/controller.py", line 435, in _get_schema_as_json
    schema = self.get_schema_from_request(request).to_schema()
  File "**/.venv/lib/python3.10/site-packages/litestar/openapi/controller.py", line 85, in get_schema_from_request
    return request.app.openapi_schema
  File "**/.venv/lib/python3.10/site-packages/litestar/app.py", line 584, in openapi_schema
    self.update_openapi_schema()
  File "**/.venv/lib/python3.10/site-packages/litestar/app.py", line 842, in update_openapi_schema
    path_item, created_operation_ids = create_path_item(
  File "**/.venv/lib/python3.10/site-packages/litestar/_openapi/path_item.py", line 129, in create_path_item
    responses=create_responses(
  File "**/.venv/lib/python3.10/site-packages/litestar/_openapi/responses.py", line 259, in create_responses
    str(route_handler.status_code): create_success_response(route_handler, schema_creator),
  File "**/.venv/lib/python3.10/site-packages/litestar/_openapi/responses.py", line 106, in create_success_response
    result = schema_creator.for_field_definition(FieldDefinition.from_annotation(return_annotation))
  File "**/.venv/lib/python3.10/site-packages/litestar/_openapi/schema_generation/schema.py", line 283, in for_field_definition
    result = self.for_object_type(field_definition)
  File "**/.venv/lib/python3.10/site-packages/litestar/_openapi/schema_generation/schema.py", line 353, in for_object_type
    items=Schema(one_of=sort_schemas_and_references(items)) if len(items) > 1 else items[0],
  File "**/.venv/lib/python3.10/site-packages/litestar/_openapi/schema_generation/utils.py", line 17, in sort_schemas_and_references
    return sorted(values, key=lambda value: value.type if isinstance(value, Schema) else value.ref)  # type: ignore
TypeError: '<' not supported between instances of 'NoneType' and 'OpenAPIType'
Traceback (most recent call last):
  File "**/.venv/lib/python3.10/site-packages/litestar/middleware/exceptions/middleware.py", line 191, in __call__
    await self.app(scope, receive, send)
  File "**/.venv/lib/python3.10/site-packages/litestar/routes/http.py", line 81, in handle
    response = await self._get_response_for_request(
  File "**/.venv/lib/python3.10/site-packages/litestar/routes/http.py", line 133, in _get_response_for_request
    return await self._call_handler_function(
  File "**/.venv/lib/python3.10/site-packages/litestar/routes/http.py", line 153, in _call_handler_function
    response_data, cleanup_group = await self._get_response_data(
  File "**/.venv/lib/python3.10/site-packages/litestar/routes/http.py", line 199, in _get_response_data
    data = route_handler.fn.value(**parsed_kwargs)
  File "**/.venv/lib/python3.10/site-packages/litestar/openapi/controller.py", line 173, in retrieve_schema_json
    body=self._get_schema_as_json(request),
  File "**/.venv/lib/python3.10/site-packages/litestar/openapi/controller.py", line 435, in _get_schema_as_json
    schema = self.get_schema_from_request(request).to_schema()
  File "**/.venv/lib/python3.10/site-packages/litestar/openapi/controller.py", line 85, in get_schema_from_request
    return request.app.openapi_schema
  File "**/.venv/lib/python3.10/site-packages/litestar/app.py", line 584, in openapi_schema
    self.update_openapi_schema()
  File "**/.venv/lib/python3.10/site-packages/litestar/app.py", line 842, in update_openapi_schema
    path_item, created_operation_ids = create_path_item(
  File "**/.venv/lib/python3.10/site-packages/litestar/_openapi/path_item.py", line 129, in create_path_item
    responses=create_responses(
  File "**/.venv/lib/python3.10/site-packages/litestar/_openapi/responses.py", line 259, in create_responses
    str(route_handler.status_code): create_success_response(route_handler, schema_creator),
  File "**/.venv/lib/python3.10/site-packages/litestar/_openapi/responses.py", line 106, in create_success_response
    result = schema_creator.for_field_definition(FieldDefinition.from_annotation(return_annotation))
  File "**/.venv/lib/python3.10/site-packages/litestar/_openapi/schema_generation/schema.py", line 283, in for_field_definition
    result = self.for_object_type(field_definition)
  File "**/.venv/lib/python3.10/site-packages/litestar/_openapi/schema_generation/schema.py", line 353, in for_object_type
    items=Schema(one_of=sort_schemas_and_references(items)) if len(items) > 1 else items[0],
  File "**/.venv/lib/python3.10/site-packages/litestar/_openapi/schema_generation/utils.py", line 17, in sort_schemas_and_references
    return sorted(values, key=lambda value: value.type if isinstance(value, Schema) else value.ref)  # type: ignore
TypeError: '<' not supported between instances of 'NoneType' and 'OpenAPIType'
INFO:     127.0.0.1:45006 - "GET /schema/openapi.json HTTP/1.1" 500 Internal Server Error
```


### Litestar Version

2.2.1

### Platform

- [X] Linux
- [ ] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)

<!-- POLAR PLEDGE BADGE START -->
---
> [!NOTE]  
> While we are open for sponsoring on [GitHub Sponsors](https://github.com/sponsors/litestar-org/) and 
> [OpenCollective](https://opencollective.com/litestar), we also utilize [Polar.sh](https://polar.sh/) to engage in pledge-based sponsorship.
>
> Check out all issues funded or available for funding [on our Polar.sh Litestar dashboard](https://polar.sh/litestar-org)
> * If you would like to see an issue prioritized, make a pledge towards it!
> * We receive the pledge once the issue is completed & verified
> * This, along with engagement in the community, helps us know which features are a priority to our users.

<a href="https://polar.sh/litestar-org/litestar/issues/2563">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/2563/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/2563/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:
Confirmed, thanks for reporting the issue @vfos-github!

Resolving patch:
diff --git a/litestar/_openapi/schema_generation/schema.py b/litestar/_openapi/schema_generation/schema.py
--- a/litestar/_openapi/schema_generation/schema.py
+++ b/litestar/_openapi/schema_generation/schema.py
@@ -42,9 +42,6 @@
     create_numerical_constrained_field_schema,
     create_string_constrained_field_schema,
 )
-from litestar._openapi.schema_generation.utils import (
-    sort_schemas_and_references,
-)
 from litestar.datastructures.upload_file import UploadFile
 from litestar.exceptions import ImproperlyConfiguredException
 from litestar.openapi.spec import Reference
@@ -365,9 +362,9 @@ def for_union_field(self, field_definition: FieldDefinition) -> Schema:
         Returns:
             A schema instance.
         """
-        inner_types = [f for f in (field_definition.inner_types or []) if not self.is_undefined(f.annotation)]
+        inner_types = (f for f in (field_definition.inner_types or []) if not self.is_undefined(f.annotation))
         values = list(map(self.for_field_definition, inner_types))
-        return Schema(one_of=sort_schemas_and_references(values))
+        return Schema(one_of=values)
 
     def for_object_type(self, field_definition: FieldDefinition) -> Schema:
         """Create schema for object types (dict, Mapping, list, Sequence etc.) types.
@@ -394,7 +391,7 @@ def for_object_type(self, field_definition: FieldDefinition) -> Schema:
             items = list(map(self.for_field_definition, inner_types or ()))
             return Schema(
                 type=OpenAPIType.ARRAY,
-                items=Schema(one_of=sort_schemas_and_references(items)) if len(items) > 1 else items[0],
+                items=Schema(one_of=items) if len(items) > 1 else items[0],
             )
 
         if field_definition.is_literal:
@@ -613,10 +610,7 @@ def for_collection_constrained_field(self, field_definition: FieldDefinition) ->
         item_creator = self.not_generating_examples
         if field_definition.inner_types:
             items = list(map(item_creator.for_field_definition, field_definition.inner_types))
-            if len(items) > 1:
-                schema.items = Schema(one_of=sort_schemas_and_references(items))
-            else:
-                schema.items = items[0]
+            schema.items = Schema(one_of=items) if len(items) > 1 else items[0]
         else:
             schema.items = item_creator.for_field_definition(
                 FieldDefinition.from_kwarg(
diff --git a/litestar/_openapi/schema_generation/utils.py b/litestar/_openapi/schema_generation/utils.py
deleted file mode 100644
--- a/litestar/_openapi/schema_generation/utils.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from __future__ import annotations
-
-from litestar.openapi.spec import Reference, Schema
-
-__all__ = ("sort_schemas_and_references",)
-
-
-def sort_schemas_and_references(values: list[Schema | Reference]) -> list[Schema | Reference]:
-    """Sort schemas and references alphabetically
-
-    Args:
-        values: A list of schemas or references.
-
-    Returns:
-        A sorted list of schemas or references
-    """
-    return sorted(values, key=lambda value: value.type if isinstance(value, Schema) else value.ref)  # type: ignore


Test patch:
diff --git a/tests/unit/test_contrib/test_pydantic/test_openapi.py b/tests/unit/test_contrib/test_pydantic/test_openapi.py
--- a/tests/unit/test_contrib/test_pydantic/test_openapi.py
+++ b/tests/unit/test_contrib/test_pydantic/test_openapi.py
@@ -163,17 +163,21 @@ def test_create_collection_constrained_field_schema_sub_fields(
         field_definition = FieldDefinition.from_annotation(annotation)
         schema = SchemaCreator().for_collection_constrained_field(field_definition)
         assert schema.type == OpenAPIType.ARRAY
-        expected = {
-            "items": {"oneOf": [{"type": "integer"}, {"type": "string"}]},
-            "maxItems": 10,
-            "minItems": 1,
-            "type": "array",
-        }
+        assert schema.max_items == 10
+        assert schema.min_items == 1
+        assert isinstance(schema.items, Schema)
+        assert schema.items.one_of is not None
+
+        def _get_schema_type(s: Any) -> OpenAPIType:
+            assert isinstance(s, Schema)
+            assert isinstance(s.type, OpenAPIType)
+            return s.type
+
+        # https://github.com/litestar-org/litestar/pull/2570#issuecomment-1788122570
+        assert {_get_schema_type(s) for s in schema.items.one_of} == {OpenAPIType.STRING, OpenAPIType.INTEGER}
         if pydantic_fn is conset:
             # set should have uniqueItems always
-            expected["uniqueItems"] = True
-
-        assert schema.to_schema() == expected
+            assert schema.unique_items
 
 
 @pytest.mark.parametrize("annotation", constrained_string_v1)
diff --git a/tests/unit/test_openapi/test_parameters.py b/tests/unit/test_openapi/test_parameters.py
--- a/tests/unit/test_openapi/test_parameters.py
+++ b/tests/unit/test_openapi/test_parameters.py
@@ -13,7 +13,7 @@
 from litestar.enums import ParamType
 from litestar.exceptions import ImproperlyConfiguredException
 from litestar.openapi import OpenAPIConfig
-from litestar.openapi.spec import Example, OpenAPI
+from litestar.openapi.spec import Example, OpenAPI, Schema
 from litestar.openapi.spec.enums import OpenAPIType
 from litestar.params import Dependency, Parameter
 from litestar.testing import create_test_client
@@ -101,26 +101,26 @@ def test_create_parameters(person_controller: Type[Controller]) -> None:
     assert gender.param_in == ParamType.QUERY
     assert gender.name == "gender"
     assert is_schema_value(gender.schema)
-    assert gender.schema.to_schema() == {
-        "oneOf": [
-            {"type": "null"},
-            {
-                "items": {
-                    "type": "string",
-                    "enum": ["M", "F", "O", "A"],
-                    "examples": [{"description": "Example  value", "value": "F"}],
-                },
-                "type": "array",
-                "examples": [{"description": "Example  value", "value": ["A"]}],
-            },
-            {
-                "type": "string",
-                "enum": ["M", "F", "O", "A"],
-                "examples": [{"description": "Example  value", "value": "M"}],
-            },
+    assert gender.schema == Schema(
+        one_of=[
+            Schema(type=OpenAPIType.NULL),
+            Schema(
+                type=OpenAPIType.STRING,
+                enum=["M", "F", "O", "A"],
+                examples=[Example(description="Example  value", value="M")],
+            ),
+            Schema(
+                type=OpenAPIType.ARRAY,
+                items=Schema(
+                    type=OpenAPIType.STRING,
+                    enum=["M", "F", "O", "A"],
+                    examples=[Example(description="Example  value", value="F")],
+                ),
+                examples=[Example(description="Example  value", value=["A"])],
+            ),
         ],
-        "examples": [{"value": "M"}, {"value": ["M", "O"]}],
-    }
+        examples=[Example(value="M"), Example(value=["M", "O"])],
+    )
     assert not gender.required
 
     assert secret_header.param_in == ParamType.HEADER
diff --git a/tests/unit/test_openapi/test_schema.py b/tests/unit/test_openapi/test_schema.py
--- a/tests/unit/test_openapi/test_schema.py
+++ b/tests/unit/test_openapi/test_schema.py
@@ -366,12 +366,12 @@ def test_schema_generation_with_generic_classes_constrained() -> None:
     assert properties["constrained"] == Schema(
         one_of=[Schema(type=OpenAPIType.INTEGER), Schema(type=OpenAPIType.STRING)]
     )
-    assert properties["union"] == Schema(one_of=[Schema(type=OpenAPIType.BOOLEAN), Schema(type=OpenAPIType.OBJECT)])
+    assert properties["union"] == Schema(one_of=[Schema(type=OpenAPIType.OBJECT), Schema(type=OpenAPIType.BOOLEAN)])
     assert properties["union_constrained"] == Schema(
-        one_of=[Schema(type=OpenAPIType.BOOLEAN), Schema(type=OpenAPIType.INTEGER), Schema(type=OpenAPIType.STRING)]
+        one_of=[Schema(type=OpenAPIType.INTEGER), Schema(type=OpenAPIType.STRING), Schema(type=OpenAPIType.BOOLEAN)]
     )
     assert properties["union_bound"] == Schema(
-        one_of=[Schema(type=OpenAPIType.BOOLEAN), Schema(type=OpenAPIType.INTEGER)]
+        one_of=[Schema(type=OpenAPIType.INTEGER), Schema(type=OpenAPIType.BOOLEAN)]
     )
 
 
@@ -404,3 +404,13 @@ def test_schema_generation_with_ellipsis() -> None:
     assert isinstance(schema, Schema)
     assert isinstance(schema.items, Schema)
     assert schema.items.type == OpenAPIType.INTEGER
+
+
+def test_schema_tuple_with_union() -> None:
+    schema = SchemaCreator().for_field_definition(FieldDefinition.from_annotation(Tuple[int, Union[int, str]]))
+    assert isinstance(schema, Schema)
+    assert isinstance(schema.items, Schema)
+    assert schema.items.one_of == [
+        Schema(type=OpenAPIType.INTEGER),
+        Schema(one_of=[Schema(type=OpenAPIType.INTEGER), Schema(type=OpenAPIType.STRING)]),
+    ]