Skip to content

Complex Request Data in Esmerald: Advanced Guide

Esmerald allows for highly flexible handling of request data. Since release 3.4+, Esmerald supports multiple payloads, letting you structure data more clearly. This feature makes your request data more organized and manageable, especially when dealing with multiple entities.

📦 Sending Multiple Payloads

Esmerald allows you to declare multiple payloads in a handler, enabling better organization of complex data. You can use the data or payload field to achieve this, with each one serving the same purpose.

Example: Using data or payload

from pydantic import BaseModel, EmailStr
from esmerald import Esmerald, Gateway, post

class User(BaseModel):
    name: str
    email: EmailStr
    hobbies: list[str]  # Pydantic model handles type validation

@post("/create")
async def create_user(data: User) -> None:
    pass

app = Esmerald(routes=[Gateway(handler=create_user)])

Request Payload (for data or payload):

{
    "name": "John",
    "email": "john.doe@example.com",
    "hobbies": ["running", "swimming"]
}

🏠 Splitting Data by Responsibility

Sometimes, you might want to send data that represents different entities (like a User and an Address). Esmerald allows you to handle such cases by splitting the payload into separate parts.

Example: User and Address

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:
    pass

app = Esmerald(routes=[Gateway(handler=create_user)])

Request Payload:

{
    "user": {
        "name": "John",
        "email": "john.doe@example.com"
    },
    "address": {
        "street_name": "123 Queens Park",
        "post_code": "90241"
    }
}

In this case, Esmerald automatically maps the user and address parts of the request to their respective models.


🧩 Optional Fields in Payloads

You can also mark fields as optional, which makes them not required for validation.

Example: Optional Address

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) -> None:
    pass

app = Esmerald(routes=[Gateway(handler=create_user)])

Request Payload (Optional Address):

  1. With Address:
{
    "user": {
        "name": "John",
        "email": "john.doe@example.com"
    },
    "address": {
        "street_name": "123 Queens Park",
        "post_code": "90241"
    }
}
  1. Without Address:
{
    "user": {
        "name": "John",
        "email": "john.doe@example.com"
    }
}

In this example, the address is optional. If it's not provided, Esmerald will still process the user data.


🔄 Using Different Encoders

Esmerald supports multiple encoders, such as Pydantic and Msgspec. This allows you to mix and match encoders based on your data needs.

Example: Using Pydantic and Msgspec

from pydantic import BaseModel, EmailStr
from msgspec import Struct
from typing import Union
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) -> None:
    pass

app = Esmerald(routes=[Gateway(handler=create_user)])

Request Payload:

{
    "user": {
        "name": "John",
        "email": "john.doe@example.com"
    },
    "address": {
        "street_name": "123 Queens Park",
        "post_code": "90241"
    }
}

Esmerald automatically handles the encoder types (Pydantic for User and Msgspec for Address), so you don't have to manually process the encoding and decoding of your data.


⚠️ Important Note on Complex Bodies

Once you add an extra body (like address), you must declare it explicitly in your handler.

Example of Declaring Complex Request Data:

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(data: User, address: Union[Address, None] = None) -> None:
    ...

app = Esmerald(routes=[Gateway(handler=create_user)])

Request Payload (Explicit Declaration):

{
    "data": {
        "name": "John",
        "email": "john.doe@example.com"
    },
    "address": {
        "street_name": "123 Queens Park",
        "post_code": "90241"
    }
}

Esmerald requires the explicit declaration of each part of the payload when working with complex bodies.


📌 Conclusion

With Esmerald, you can easily handle complex request data, split data into multiple parts, and use advanced techniques like optional fields and different encoders. Whether you're handling a single object or complex nested data, Esmerald's flexibility ensures you can build scalable, well-structured APIs with ease.