Repository: litestar-org/litestar

Issue:
Bug: Annotated Types not being rendered in OpenAPI schema
### Description

I've got a `msgspec.Struct` request object that uses an `typing.Annotated` type as one of it's field types.

When viewing routes request object, I'm only seeing a str or null allowed but no mention of the max length piece.  This also happens when I exclude the `| None` from the type declaration as well.

### URL to code causing the issue

_No response_

### MCVE

```python
from typing import Annotated
import msgspec

IDType = Annotated[str, msgspec.Meta(max_length=16)]

class Lookup(msgspec.Struct):
    id:  IDType | None
```


### Steps to reproduce

```bash
1. Create route with the above request object.
2. View the route in the /schema endpoint.
```


### Screenshots

_No response_

### Logs

_No response_

### Litestar Version

2.0.0a5

### Platform

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


Discussion:
Yes, im not sure we currently parse the msgspec meta data. 
There is also an issue with `Optional[...]` that @v3ss0n has encountered where type of those fields is displaying as `"Unknown type: null"` in api docs (not saying related, just that there is seemingly work to be done on this in general).

I'd like us to get to handling these msgspec `Annotated` constraints as I intend to improve on the SQLAlchemy and other ORM DTOs to generate them when possible, e.g., `VARCHAR(16)` would generate `Annotated[str, msgspec.Meta(max_length=16)]` on the transfer model.
Aight, if no one picks it I'll take it depending on my capacity 
> There is also an issue with `Optional[...]` that @v3ss0n has encountered where type of those fields is displaying as `"Unknown type: null"` in api docs (not saying related, just that there is seemingly work to be done on this in general).
> 
> I'd like us to get to handling these msgspec `Annotated` constraints as I intend to improve on the SQLAlchemy and other ORM DTOs to generate them when possible, e.g., `VARCHAR(16)` would generate `Annotated[str, msgspec.Meta(max_length=16)]` on the transfer model.

Cool, this got tracked. 
I was about to open an issue on it , so this one should cover right?

Resolving patch:
diff --git a/litestar/_signature/field.py b/litestar/_signature/field.py
--- a/litestar/_signature/field.py
+++ b/litestar/_signature/field.py
@@ -3,7 +3,8 @@
 from dataclasses import dataclass
 from typing import Any, Literal
 
-from typing_extensions import get_args, get_origin
+from msgspec import Meta
+from typing_extensions import Annotated, get_args, get_origin
 
 from litestar.constants import UNDEFINED_SENTINELS
 from litestar.params import BodyKwarg, DependencyKwarg, ParameterKwarg
@@ -22,6 +23,26 @@
 __all__ = ("SignatureField",)
 
 
+def _create_metadata_from_type(
+    value: Any, model: type[ParameterKwarg] | type[BodyKwarg], field_type: Any
+) -> ParameterKwarg | BodyKwarg | None:
+    if isinstance(value, Meta):
+        is_sequence_container = is_non_string_sequence(field_type)
+        return model(
+            gt=value.gt,
+            ge=value.ge,
+            lt=value.lt,
+            le=value.le,
+            multiple_of=value.multiple_of,
+            regex=value.pattern,
+            min_length=value.min_length if not is_sequence_container else None,
+            max_length=value.max_length if not is_sequence_container else None,
+            min_items=value.min_length if is_sequence_container else None,
+            max_items=value.max_length if is_sequence_container else None,
+        )
+    return None
+
+
 @dataclass(unsafe_hash=True, frozen=True)
 class SignatureField:
     """Abstraction representing a model field. This class is meant to replace equivalent datastructures from other
@@ -170,8 +191,17 @@ def create(
         if kwarg_model and default_value is Empty:
             default_value = kwarg_model.default
 
-        if not children and get_origin(field_type) and (type_args := get_args(field_type)):
-            children = tuple(SignatureField.create(arg) for arg in type_args)
+        origin = get_origin(field_type)
+
+        if not children and origin and (type_args := get_args(field_type)):
+            if origin is Annotated:
+                field_type = type_args[0]
+                if not kwarg_model:
+                    kwarg_model = _create_metadata_from_type(
+                        type_args[1], BodyKwarg if name == "data" else ParameterKwarg, field_type=field_type
+                    )
+            else:
+                children = tuple(SignatureField.create(arg) for arg in type_args)
 
         return SignatureField(
             name=name,


Test patch:
diff --git a/tests/openapi/test_schema.py b/tests/openapi/test_schema.py
--- a/tests/openapi/test_schema.py
+++ b/tests/openapi/test_schema.py
@@ -2,8 +2,10 @@
 from enum import Enum
 from typing import TYPE_CHECKING, Dict, Literal
 
+import msgspec
 import pytest
 from pydantic import BaseModel
+from typing_extensions import Annotated
 
 from litestar import Controller, MediaType, get
 from litestar._openapi.schema_generation.schema import (
@@ -245,3 +247,17 @@ class Foo(TypedDict):
     )
     schema = create_schema_for_typed_dict(module.Foo, generate_examples=False, plugins=[], schemas={})
     assert schema.properties and all(key in schema.properties for key in ("foo", "bar", "baz"))
+
+
+def test_create_schema_from_msgspec_annotated_type() -> None:
+    class Lookup(msgspec.Struct):
+        id: Annotated[str, msgspec.Meta(max_length=16)]
+
+    schemas: Dict[str, Schema] = {}
+    create_schema(
+        field=SignatureField.create(name="Lookup", field_type=Lookup),
+        generate_examples=False,
+        plugins=[],
+        schemas=schemas,
+    )
+    assert schemas["Lookup"]