Skip to content

Commit

Permalink
docs: Document custom types (#3603)
Browse files Browse the repository at this point in the history
* document how to tell litestar to en/decode a custum type via msgspec

* fixes from linting checks

* Apply suggestions from review

Co-authored-by: Jacob Coffee <[email protected]>

* add example for pydantic custom type

* present type encoders/decoders example as well as Pydantic custom class example

* fix test for Python 3.8

* fix linting error

---------

Co-authored-by: stewit <>
Co-authored-by: Jacob Coffee <[email protected]>
  • Loading branch information
stewit and JacobCoffee authored Aug 25, 2024
1 parent 60e17cd commit 5223d2d
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 3 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ to test them alongside the rest of the test suite, ensuring they do not become s
Please follow the next guidelines when adding a new example:

- Add the example in the corresponding module directory in ``/docs/examples`` or create a new one if necessary
- Create a suite for the module in ``/docs/examples/tests`` that tests the aspects of the example that it demonstrates
- Create a suite for the module in ``/tests/examples`` that tests the aspects of the example that it demonstrates
- Reference the example in the rst file with an external reference code block, e.g.

.. code-block:: rst
Expand Down
Empty file.
77 changes: 77 additions & 0 deletions docs/examples/encoding_decoding/custom_type_encoding_decoding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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: /asset -X POST -H "Content-Type: application/json" -d '{"name":"SomeAsset","user":"TenantA_Somebody"}'
65 changes: 65 additions & 0 deletions docs/examples/encoding_decoding/custom_type_pydantic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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: /asset -X POST -H "Content-Type: application/json" -d '{"name":"SomeAsset","user":"TenantA_Somebody"}'
4 changes: 2 additions & 2 deletions docs/usage/applications.rst
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,6 @@ Parameters that support layering are:
* :doc:`return_dto </usage/dto/0-basic-use>`
* ``security``
* ``tags``
* ``type_decoders``
* ``type_encoders``
* :doc:`type_decoders </usage/custom-types>`
* :doc:`type_encoders </usage/custom-types>`
* :ref:`websocket_class <usage/websockets:custom websocket>`
34 changes: 34 additions & 0 deletions docs/usage/custom-types.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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`` :term:`parameters <parameter>` which can be defined on every layer. For example see the :doc:`litestar app reference </reference/app>`.

.. admonition:: 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:
:ref:`Layered architecture <usage/applications:layered architecture>`

Here is an example:

.. literalinclude:: /examples/encoding_decoding/custom_type_encoding_decoding.py
:caption: Tell Litestar how to encode and decode a custom type

Custom Pydantic types
---------------------

If you use a custom Pydantic type you can use it directly:

.. literalinclude:: /examples/encoding_decoding/custom_type_pydantic.py
:caption: Tell Litestar how to encode and decode a custom Pydantic type
1 change: 1 addition & 0 deletions docs/usage/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Usage
responses
security/index
static-files
custom-types
stores
templating
testing
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from docs.examples.encoding_decoding.custom_type_encoding_decoding import app

from litestar.status_codes import HTTP_201_CREATED
from litestar.testing import TestClient


def test_custom_type_encoding_decoding_works() -> None:
with TestClient(app) as client:
response = client.post(
"/asset",
json={
"user": "TenantA_Somebody",
"name": "Some Asset",
},
)

assert response.status_code == HTTP_201_CREATED
17 changes: 17 additions & 0 deletions tests/examples/test_encoding_decoding/test_custom_type_pydantic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from docs.examples.encoding_decoding.custom_type_pydantic import app

from litestar.status_codes import HTTP_201_CREATED
from litestar.testing import TestClient


def test_custom_type_encoding_decoding_works() -> None:
with TestClient(app) as client:
response = client.post(
"/asset",
json={
"user": "TenantA_Somebody",
"name": "Some Asset",
},
)

assert response.status_code == HTTP_201_CREATED

0 comments on commit 5223d2d

Please sign in to comment.