Skip to content

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 ways of doing this, using the data or using 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)])

Summary

  • To process a payload it must have a data or a payload field declared in the handler.
  • data or payload can be any type, including pydantic models.
  • Validations can be achieved by:
    • Using the Field from pydantic and automatic delegate the validations to it.
    • Using custom validations.
  • To make a field non-mandatory you must use the Optional.