Custom types#
Data serialization / deserialization (encoding / decoding) and validation are important parts of any API framework.
In addition to being capable to encode / decode and validate many standard types, litestar supports Python’s builtin dataclasses and libraries like Pydantic and msgspec.
However, sometimes you may need to employ a custom type.
Using type encoders / decoders#
Litestar supports a mechanism where you provide encoding and decoding hook functions which translate your type in / to a type that it knows. You can provide them via the type_encoders
and type_decoders
parameters which can be defined on every layer. For example see the litestar app reference.
Layered architecture
type_encoders
and type_decoders
are part of Litestar’s layered architecture, which means you can set them on every layer of the application. If you set them on multiple layers,
the layer closest to the route handler will take precedence.
You can read more about this here: Layered architecture
Here is an example:
from typing import Any, Type
from msgspec import Struct
from litestar import Litestar, post
class TenantUser:
"""Custom Type that represents a user associated to a tenant
Parsed from / serialized to a combined tenant + user id string of the form
TENANTPREFIX_USERID
i.e. separated by underscore.
"""
tenant_prefix: str
user_id: str
def __init__(self, tenant_prefix: str, user_id: str) -> None:
self.tenant_prefix = tenant_prefix
self.user_id = user_id
@classmethod
def from_string(cls, s: str) -> "TenantUser":
splits = s.split("_", maxsplit=1)
if len(splits) < 2:
raise ValueError(
"Could not split up tenant user id string. "
"Expecting underscore for separation of tenant prefix and user id."
)
return cls(tenant_prefix=splits[0], user_id=splits[1])
def to_combined_str(self) -> str:
return self.tenant_prefix + "_" + self.user_id
def tenant_user_type_predicate(type: Type) -> bool:
return type is TenantUser
def tenant_user_enc_hook(u: TenantUser) -> Any:
return u.to_combined_str()
def tenant_user_dec_hook(tenant_user_id_str: str) -> TenantUser:
return TenantUser.from_string(tenant_user_id_str)
def general_dec_hook(type: Type, obj: Any) -> Any:
if tenant_user_type_predicate(type):
return tenant_user_dec_hook(obj)
raise NotImplementedError(f"Encountered unknown type during decoding: {type!s}")
class UserAsset(Struct):
user: TenantUser
name: str
@post("/asset", sync_to_thread=False)
def create_asset(
data: UserAsset,
) -> UserAsset:
assert isinstance(data.user, TenantUser)
return data
app = Litestar(
[create_asset],
type_encoders={TenantUser: tenant_user_enc_hook}, # tell litestar how to encode TenantUser
type_decoders=[(tenant_user_type_predicate, general_dec_hook)], # tell litestar how to decode TenantUser
)
Run it
> curl http://127.0.0.1:8000/asset -X POST -H Content-Type: application/json -d {"name":"SomeAsset","user":"TenantA_Somebody"}
{"user":"TenantA_Somebody","name":"SomeAsset"}
Custom Pydantic types#
If you use a custom Pydantic type you can use it directly:
from pydantic import BaseModel, BeforeValidator, ConfigDict, PlainSerializer, WithJsonSchema
from typing_extensions import Annotated
from litestar import Litestar, post
class TenantUser:
"""Custom Type that represents a user associated to a tenant
Parsed from / serialized to a combined tenant + user id string of the form
TENANTPREFIX_USERID
i.e. separated by underscore.
"""
tenant_prefix: str
user_id: str
def __init__(self, tenant_prefix: str, user_id: str) -> None:
self.tenant_prefix = tenant_prefix
self.user_id = user_id
@classmethod
def from_string(cls, s: str) -> "TenantUser":
splits = s.split("_", maxsplit=1)
if len(splits) < 2:
raise ValueError(
"Could not split up tenant user id string. "
"Expecting underscore for separation of tenant prefix and user id."
)
return cls(tenant_prefix=splits[0], user_id=splits[1])
def to_combined_str(self) -> str:
return self.tenant_prefix + "_" + self.user_id
PydAnnotatedTenantUser = Annotated[
TenantUser,
BeforeValidator(lambda x: TenantUser.from_string(x)),
PlainSerializer(lambda x: x.to_combined_str(), return_type=str),
WithJsonSchema({"type": "string"}, mode="serialization"),
]
class UserAsset(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
user: PydAnnotatedTenantUser
name: str
@post("/asset", sync_to_thread=False)
def create_asset(
data: UserAsset,
) -> UserAsset:
assert isinstance(data.user, TenantUser)
return data
app = Litestar(
[create_asset],
)
Run it
> curl http://127.0.0.1:8000/asset -X POST -H Content-Type: application/json -d {"name":"SomeAsset","user":"TenantA_Somebody"}
{"user":"TenantA_Somebody","name":"SomeAsset"}