Request Data¶
In every application there will be times where sending a payload to the server will be needed.
Esmerald is prepared to handle those with ease and that is thanks to Pydantic.
There are two default ways of doing this, using the data and using the payload but there are also custom ways of creating the payload.
Warning
You can only declare data
or payload
in the handler but not both or an ImproperlyConfigured
exception is raised.
The data
field¶
When sending a payload to the backend to be validated, the handler needs to have a data
field declared. Without it,
it will not be possible to process the information and/or will not be recognised.
from pydantic import BaseModel, EmailStr
from esmerald import Esmerald, Gateway, post
class User(BaseModel):
name: str
email: EmailStr
@post("/create")
async def create_user(data: User) -> None:
"""
Creates a user in the system and does not return anything.
Default status_code: 201
"""
app = Esmerald(routes=[Gateway(handler=create_user)])
Fundamentally the data
field is what holds the information about the sent payload data into the server.
The data can also be simple types such as list
, dict
, str
. It does not necessarily mean you need to always use
pydantic models.
The payload
field¶
Fundamentally is an alternative to data
but does exactly the same. If you are more familiar with
the concept of payload
then this is for you.
from pydantic import BaseModel, EmailStr
from esmerald import Esmerald, Gateway, post
class User(BaseModel):
name: str
email: EmailStr
@post("/create")
async def create_user(payload: User) -> None:
"""
Creates a user in the system and does not return anything.
Default status_code: 201
"""
app = Esmerald(routes=[Gateway(handler=create_user)])
Nested models¶
You can also do nested models for the data
or payload
to be processed.
from pydantic import BaseModel, EmailStr
from esmerald import Esmerald, Gateway, post
class Address(BaseModel):
zip_code: str
country: str
street: str
region: str
class User(BaseModel):
name: str
email: EmailStr
address: Address
@post("/create")
async def create_user(data: User) -> None:
"""
Creates a user in the system and does not return anything.
Default status_code: 201
"""
app = Esmerald(routes=[Gateway(handler=create_user)])
from pydantic import BaseModel, EmailStr
from esmerald import Esmerald, Gateway, post
class Address(BaseModel):
zip_code: str
country: str
street: str
region: str
class User(BaseModel):
name: str
email: EmailStr
address: Address
@post("/create")
async def create_user(payload: User) -> None:
"""
Creates a user in the system and does not return anything.
Default status_code: 201
"""
app = Esmerald(routes=[Gateway(handler=create_user)])
The data expected to be sent to be validated is all required and expected with the following format:
{
"name": "John",
"email": "john.doe@example.com",
"address": {
"zip_code": "90210",
"country": "United States",
"street": "Orange county street",
"region": "California"
}
}
You can nest as many models as you wish to nest as long as it is send in the right format.
Mandatory fields¶
There are many ways to process and validate a field and also the option to make it non mandatory.
That can be achieved by using the typing Optional
to make it not mandatory.
from typing import Optional
from pydantic import BaseModel, EmailStr
from esmerald import Esmerald, Gateway, post
class Address(BaseModel):
zip_code: str
country: str
street: Optional[str]
region: Optional[str]
class User(BaseModel):
name: str
email: EmailStr
address: Optional[Address]
@post("/create")
async def create_user(data: User) -> None:
"""
Creates a user in the system and does not return anything.
Default status_code: 201
"""
app = Esmerald(routes=[Gateway(handler=create_user)])
from typing import Optional
from pydantic import BaseModel, EmailStr
from esmerald import Esmerald, Gateway, post
class Address(BaseModel):
zip_code: str
country: str
street: Optional[str]
region: Optional[str]
class User(BaseModel):
name: str
email: EmailStr
address: Optional[Address]
@post("/create")
async def create_user(payload: User) -> None:
"""
Creates a user in the system and does not return anything.
Default status_code: 201
"""
app = Esmerald(routes=[Gateway(handler=create_user)])
The address
is not mandatory to be send in the payload and therefore it can be done like this:
{
"name": "John",
"email": "john.doe@example.com"
}
But you can also send the address and inside without the mandatory fields:
{
"name": "John",
"email": "john.doe@example.com",
"address": {
"zip_code": "90210",
"country": "United States"
}
}
Field validation¶
What about the field validation? What if you need to validate some of the data being sent to the backend?
Since Esmerald uses pydantic, you can take advantage of it.
from typing import List
from pydantic import BaseModel, EmailStr, Field
from esmerald import Esmerald, Gateway, post
class User(BaseModel):
name: str = Field(min_length=3)
email: EmailStr
hobbies: List[str] = Field(min_items=3)
age: int = Field(ge=18)
@post("/create")
async def create_user(data: User) -> None:
"""
Creates a user in the system and does not return anything.
Default status_code: 201
"""
app = Esmerald(routes=[Gateway(handler=create_user)])
from typing import List
from pydantic import BaseModel, EmailStr, Field
from esmerald import Esmerald, Gateway, post
class User(BaseModel):
name: str = Field(min_length=3)
email: EmailStr
hobbies: List[str] = Field(min_items=3)
age: int = Field(ge=18)
@post("/create")
async def create_user(payload: User) -> None:
"""
Creates a user in the system and does not return anything.
Default status_code: 201
"""
app = Esmerald(routes=[Gateway(handler=create_user)])
Since pydantic runs the validations internally, you will have the errors thrown if something is missing.
The expected payload would be:
{
"name": "John",
"email": "john.doe@example.com",
"hobbies": [
"running",
"swimming",
"Netflix bing watching"
],
"age": 18
}
Custom field validation¶
You don't necessarily need to use the pydantic default validation for your fields. You can always apply one of your own.
from typing import List
from pydantic import BaseModel, EmailStr, Field, validator
from esmerald import Esmerald, Gateway, post
class User(BaseModel):
name: str
email: EmailStr
hobbies: List[str] = Field(min_items=3)
age: int
@validator("age", always=True)
def validate_age(cls, value: int) -> int:
"""
Validates the age of a user.
"""
if value < 18:
raise ValueError("The age must be at least 18.")
return value
@validator("name")
def validate_name(cls, value: str) -> str:
"""
Validates the name of a user.
"""
if len(value) < 3:
raise ValueError("The name must be at least 3 characters.")
return value
@post("/create")
async def create_user(data: User) -> None:
"""
Creates a user in the system and does not return anything.
Default status_code: 201
"""
app = Esmerald(routes=[Gateway(handler=create_user)])
from typing import List
from pydantic import BaseModel, EmailStr, Field, validator
from esmerald import Esmerald, Gateway, post
class User(BaseModel):
name: str
email: EmailStr
hobbies: List[str] = Field(min_items=3)
age: int
@validator("age", always=True)
def validate_age(cls, value: int) -> int:
"""
Validates the age of a user.
"""
if value < 18:
raise ValueError("The age must be at least 18.")
return value
@validator("name")
def validate_name(cls, value: str) -> str:
"""
Validates the name of a user.
"""
if len(value) < 3:
raise ValueError("The name must be at least 3 characters.")
return value
@post("/create")
async def create_user(payload: User) -> None:
"""
Creates a user in the system and does not return anything.
Default status_code: 201
"""
app = Esmerald(routes=[Gateway(handler=create_user)])
Complex request data¶
Since the release 3.4+, Esmerald allows you to have multiple payloads declared and allows you to customize the way you want to send it.
The data and payload will always continue to do what they are supposed to do which means the following is valid.
from typing import List
from pydantic import BaseModel, EmailStr, Field, validator
from esmerald import Esmerald, Gateway, post
class User(BaseModel):
name: str
email: EmailStr
hobbies: List[str] = Field(min_items=3)
age: int
@validator("age", always=True)
def validate_age(cls, value: int) -> int:
"""
Validates the age of a user.
"""
if value < 18:
raise ValueError("The age must be at least 18.")
return value
@validator("name")
def validate_name(cls, value: str) -> str:
"""
Validates the name of a user.
"""
if len(value) < 3:
raise ValueError("The name must be at least 3 characters.")
return value
@post("/create")
async def create_user(data: User) -> None:
"""
Creates a user in the system and does not return anything.
Default status_code: 201
"""
app = Esmerald(routes=[Gateway(handler=create_user)])
from typing import List
from pydantic import BaseModel, EmailStr, Field, validator
from esmerald import Esmerald, Gateway, post
class User(BaseModel):
name: str
email: EmailStr
hobbies: List[str] = Field(min_items=3)
age: int
@validator("age", always=True)
def validate_age(cls, value: int) -> int:
"""
Validates the age of a user.
"""
if value < 18:
raise ValueError("The age must be at least 18.")
return value
@validator("name")
def validate_name(cls, value: str) -> str:
"""
Validates the name of a user.
"""
if len(value) < 3:
raise ValueError("The name must be at least 3 characters.")
return value
@post("/create")
async def create_user(payload: User) -> None:
"""
Creates a user in the system and does not return anything.
Default status_code: 201
"""
app = Esmerald(routes=[Gateway(handler=create_user)])
But what if you want to actually have a different payload split by responsabilities or simply because you just want for organisation purposes guarantee a more complex request?
Let us imagine we want to register a user and at the same time we want to provide extra details for that user such as an address.
Well, you can use the data
or payload
to do it and send all in one go but you can also do something like this:
from pydantic import BaseModel, EmailStr
from esmerald import Esmerald, Gateway, post
class User(BaseModel):
name: str
email: EmailStr
class Address(BaseModel):
street_name: str
post_code: str
@post("/create")
async def create_user(user: User, address: Address) -> None:
"""
Creates a user in the system and does not return anything.
Default status_code: 201
"""
app = Esmerald(routes=[Gateway(handler=create_user)])
This makes the declaration of the body a bit more oriented to the type of data you want to send. So, how it would the request data would look like now?
{
"user": {
"name": "John",
"email": "john.doe@example.com",
},
"address": {
"street_name": "123 Queens Park",
"post_code": "90241"
}
}
Esmerald automatically will understand where to map the request data and assign them to the proper declaration of
the keys
sent.
Non mandatory fields in the payload¶
The same principle applied to everything in Esmerald is also applied here in the same fashion. You can also make the fields also not mandatory, something like this:
from pydantic import BaseModel, EmailStr
from typing import Union
from esmerald import Esmerald, Gateway, post
class User(BaseModel):
name: str
email: EmailStr
class Address(BaseModel):
street_name: str
post_code: str
@post("/create")
async def create_user(user: User, address: Union[Address, None]) -> None:
"""
Creates a user in the system and does not return anything.
Default status_code: 201
Union is used but it could also be `Optional`.
"""
app = Esmerald(routes=[Gateway(handler=create_user)])
Note
We use Union
but Optional
can also be used.
As you can see, now the address
is a non mandatory field and that means you can simply do:
{
"user": {
"name": "John",
"email": "john.doe@example.com",
}
}
Esmerald will know and understand what to do. Pretty simple, right?
Using different Encoders
¶
Well, Esmerald is also known for being able to mix and match multiple Encoders. If you are not familiar with those, now its a great time to go read and catch-up with those.
Now, this is very unlikely to happen where you mix encoders such as Pydantic
with Msgspec
or attrs
but it could
happen if you want, after all you are in charge of your own destiny!
Since Esmerald understands those, that means you can also have a complex payload using different encoders and it would still work as it is supposed to.
Let us see the use of two encoders at the same time and how you could do it here.
Note
We will be assuming the encoders section was read and understood and you are comfortable with the concept.
We will be using the two default supported encoders, Pydantic and MsgSpec.
Using the example from before, now the Address
won't be a Pydantic model but a Msgspec Struct
.
from pydantic import BaseModel, EmailStr
from typing import Union
from msgspec import Struct
from esmerald import Esmerald, Gateway, post
class User(BaseModel):
name: str
email: EmailStr
class Address(Struct):
street_name: str
post_code: str
@post("/create")
async def create_user(user: User, address: Union[Address, None]) -> None:
"""
Creates a user in the system and does not return anything.
Default status_code: 201
Union is used but it could also be `Optional`.
"""
app = Esmerald(routes=[Gateway(handler=create_user)])
As you can see, nothing really changed besides the type of object, since Esmerald understands the encoder type, it will automatically parse them to the proper object and run the declared validations but in terms of the way you send the request data remains exactly the same.
Sending the whole data
{
"user": {
"name": "John",
"email": "john.doe@example.com",
},
"address": {
"street_name": "123 Queens Park",
"post_code": "90241"
}
}
Sending only the mandatory one
{
"user": {
"name": "John",
"email": "john.doe@example.com",
}
}
Important note¶
From the moment you add an extra body to the signasture of your handler, you must declare them explicitly in your request, even if you want to call it data or payload, it must be there.
from pydantic import BaseModel, EmailStr
from typing import Union
from msgspec import Struct
from esmerald import Esmerald, Gateway, post
class User(BaseModel):
name: str
email: EmailStr
class Address(Struct):
street_name: str
post_code: str
@post("/create")
async def create_user(data: User, address: Union[Address, None]) -> None:
"""
Creates a user in the system and does not return anything.
Default status_code: 201
"""
app = Esmerald(routes=[Gateway(handler=create_user)])
Even if you used the data
reserved word and because the body is now in a complex form, the request must be explicitly
declared, like this:
Sending the whole data
{
"data": {
"name": "John",
"email": "john.doe@example.com",
},
"address": {
"street_name": "123 Queens Park",
"post_code": "90241"
}
}
Sending only the mandatory one
{
"data": {
"name": "John",
"email": "john.doe@example.com",
}
}