Перейти к содержанию

Encoders

Warning

The current page still doesn't have a translation for this language.

But you can help translating it: Contributing.

Esmerald being built on top of Lilya, brings another level of flexibility, the encoders.

Pretty much like Lilya, an Encoder is what allows a specific type of object to be understood, encoded and serialized by Esmerald without breaking the application.

An example of default existing encoders in Esmerald would be the support for Pydantic and MsgSpec.

Warning

The encoders came to Esmerald after the version 3.1.2. If you are using a version prior to that, this won't be available.

Benefits of encoders

The greatest benefit of supporting the encoders is that you don't need to rely on a specific framework to support a specific library for you to use.

With Esmerald Encoder you can design it yourself and simply add it to Esmerald to be used making it future proof and extremely dynamic.

How to use it

To take advantage of the Encoders you must subclass the Encoder from Esmerald and implement three mandatory functions.

from esmerald.encoders import Encoder

When subclassing the Encoder, the mandatory functions are:

Esmerald extends the native functionality of Lilya regarding the encoders and adds some extra flavours to it.

The reasoning behind it its because Esmerald internally manages signatures and data validations that are unique to Esmerald.

is_type

This function might sound confusing but it is in fact something simple. This function is used to check if the object of type X is an instance or a subclass of that same type.

Danger

Here is where it is different from Lilya. With Lilya you can use the __type__ as well but **not in Esmerald. In Esmerald you must implement the is_type function.

Example

This is what currently Esmerald is doing for Pydantic and MsgSpec.

from __future__ import annotations

from typing import Any

from msgspec import Struct
from pydantic import BaseModel

from esmerald.encoders import Encoder
from lilya._utils import is_class_and_subclass


class MsgSpecEncoder(Encoder):

    def is_type(self, value: Any) -> bool:
        return isinstance(value, Struct) or is_class_and_subclass(value, Struct)


class PydanticEncoder(Encoder):

    def is_type(self, value: Any) -> bool:
        return isinstance(value, BaseModel) or is_class_and_subclass(value, BaseModel)

As you can see, this is how we check and verify if an object of type BaseModel and Struct are properly validated by Esmerald.

serialize

This function is what tells Esmerald how to serialize the given object type into a JSON readable format.

Quite simple and intuitive.

Example

from __future__ import annotations

from typing import Any

import msgspec
from msgspec import Struct
from pydantic import BaseModel

from esmerald.encoders import Encoder
from lilya._utils import is_class_and_subclass


class MsgSpecEncoder(Encoder):

    def is_type(self, value: Any) -> bool:
        return isinstance(value, Struct) or is_class_and_subclass(value, Struct)

    def serialize(self, obj: Any) -> Any:
        return msgspec.json.decode(msgspec.json.encode(obj))


class PydanticEncoder(Encoder):

    def is_type(self, value: Any) -> bool:
        return isinstance(value, BaseModel) or is_class_and_subclass(value, BaseModel)

    def serialize(self, obj: BaseModel) -> dict[str, Any]:
        return obj.model_dump()

encode

Finally, this functionality is what converts a given piece of data (JSON usually) into an object of the type of the Encoder.

For example, a dictionary into Pydantic models or MsgSpec Structs.

Example

from __future__ import annotations

from typing import Any

import msgspec
from msgspec import Struct
from pydantic import BaseModel

from esmerald.encoders import Encoder
from lilya._utils import is_class_and_subclass


class MsgSpecEncoder(Encoder):

    def is_type(self, value: Any) -> bool:
        return isinstance(value, Struct) or is_class_and_subclass(value, Struct)

    def serialize(self, obj: Any) -> Any:
        return msgspec.json.decode(msgspec.json.encode(obj))

    def encode(self, annotation: Any, value: Any) -> Any:
        return msgspec.json.decode(msgspec.json.encode(value), type=annotation)


class PydanticEncoder(Encoder):

    def is_type(self, value: Any) -> bool:
        return isinstance(value, BaseModel) or is_class_and_subclass(value, BaseModel)

    def serialize(self, obj: BaseModel) -> dict[str, Any]:
        return obj.model_dump()

    def encode(self, annotation: Any, value: Any) -> Any:
        if isinstance(value, BaseModel):
            return value
        return annotation(**value)

The flexibility

As you can see, there are many ways of you building your encoders. Esmerald internally already brings two of them out of the box but you are free to build your own custom encoder and apply your own logic and validations.

You have 100% the power and control over any validator you would love to have in your Esmerald application.

Custom Encoders

Well, this is where it becomes interesting. What if you actually want to build an Encoder that is not currently supported by Esmerald natively, for example, the library attrs?

It is in fact very simple as well, following the previous steps and explanations, it would look like this:

from __future__ import annotations

from typing import Any

from attrs import asdict, define, field, has

from esmerald.encoders import Encoder


class AttrsEncoder(Encoder):

    def is_type(self, value: Any) -> bool:
        return has(value)

    def serialize(self, obj: Any) -> Any:
        return asdict(obj)

    def encode(self, annotation: Any, value: Any) -> Any:
        return annotation(**value)


# The way an `attr` object is defined
@define
class AttrItem:
    name: str = field()
    age: int = field()
    email: str

Do you see any differences compared to Pydantic and MsgSpec?

Well, the is_type does not check for an isinstance or is_class_and_subclass and the reason for that its because when using attrs there is not specific object of type X like we have in others, in fact, the attrs uses decorators for it and by default provides a has() function that is used to check the attrs object types, so we can simply use it.

Every library has its own ways, object types and everything in between to check and this is the reason why the is_type exists, to make sure you have the control over the way the typing is checked.

Now imagine what you can do with any other library at your choice.

Register the Encoder

Well, building the encoders is good fun but it does nothing to Esmerald unless you make it aware those in fact exist and should be used.

There are different ways of registering the encoders.

Esmerald also provides a function to register anywhere in your application but it is not recommended to use it without understanding the ramifications, mostly if you have handlers relying on a given object type that needs the encoder to be available before assembling the routing system.

from esmerald.encoders import register_esmerald_encoder

Via Settings

Like everything in Esmerald, you can use the settings for basically everything in your application.

Let us use the example of the custom encoder AttrsEncoder.

from typing import List, Union

from myapp.encoders import AttrsEncoder

from esmerald import EsmeraldAPISettings
from esmerald.encoders import Encoder


class AppSettings(EsmeraldAPISettings):
    @property
    def encoders(self) -> Union[List[Encoder], None]:
        return [AttrsEncoder]

Via Instance

Classic approach and also available in any Esmerald or ChildEsmerald instance.

from myapp.encoders import AttrsEncoder

from esmerald import Esmerald

app = Esmerald(
    routes=[...],
    encoders=[AttrsEncoder],
)

Adding an encoder via app instance function

This is also available in any Esmerald and ChildEsmerald application. If you would like to add an encoder after instantiation you can do it but again, it is not recommended to use it without understanding the ramifications, mostly if you have handlers relying on a given object type that needs the encoder to be available before assembling the routing system.

from myapp.encoders import AttrsEncoder

from esmerald import Esmerald

app = Esmerald(
    routes=[...],
)
app.register_encoder(AttrsEncoder)

Notes

Having this level of flexibility is great in any application and Esmerald makes it easy for you but it is also important to understand that this level of control also comes with risks, meaning, when you build an encoder, make sure you test all the cases possible and more importantly, you implement all the functions mentioned above or else your application will break.