Encoders¶
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.