ESC

Type to search...

Body Updates

PATCH-style partial updates for request bodies.

Basic Partial Updates

Receive partial body data and merge with existing data:

from pydantic import BaseModel, Field
from evoid.web.route import Service, patch

class UserUpdate(BaseModel):
    name: str | None = None
    email: str | None = None
    age: int | None = Field(default=None, ge=0, le=150)

# In-memory database
users_db = {
    1: {"id": 1, "name": "Alice", "email": "alice@example.com", "age": 30}
}

app = Service("api")

@app.patch("/users/{user_id}")
async def update_user(user_id: int, updates: UserUpdate):
    if user_id not in users_db:
        raise ValueError("User not found")
    
    # Merge updates with existing data
    existing = users_db[user_id]
    update_data = updates.model_dump(exclude_unset=True)
    existing.update(update_data)
    
    return {"status": "updated", "user": existing}

Required vs Optional Fields

Mix required and optional fields in updates:

from pydantic import BaseModel, Field
from evoid.web.route import Service, patch

class ProductUpdate(BaseModel):
    # Required field
    version: str
    
    # Optional fields
    name: str | None = None
    price: float | None = Field(default=None, gt=0)

app = Service("api")

@app.patch("/products/{product_id}")
async def update_product(product_id: int, updates: ProductUpdate):
    # version is always required, others are optional
    return {
        "status": "updated",
        "product_id": product_id,
        "version": updates.version,
        "changes": updates.model_dump(exclude_unset=True)
    }

Native IOP Style

In IOP, validation and data merging belong in processors:

from evoid.native import create_service, on
from evoid import Intent, Level, Context

app = create_service("api")

# Simulated database
users_db = {
    1: {"id": 1, "name": "Alice", "email": "alice@example.com", "age": 30}
}

# 1. Processor: validate user exists
async def validate_user_exists(ctx: Context) -> dict:
    user_id = int(ctx.metadata.get("path_params", {}).get("user_id", 0))
    if user_id not in users_db:
        raise ValueError("User not found")
    ctx.state["existing_user"] = users_db[user_id].copy()
    return {"validated": True}

# 2. Intent with validator in pipeline
UPDATE_USER = Intent(
    name="PATCH:/users",
    level=Level.STANDARD,
    metadata={
        "method": "PATCH",
        "path": "/users/{user_id}",
        "processors": ("validate_user_exists",),
    },
)

# 3. Handler — only merge and business logic
async def handle_update_user(intent: Intent) -> dict:
    body = intent.metadata.get("body", {})
    existing = intent.state["existing_user"]  # from processor

    # Merge non-None values
    for key, value in body.items():
        if value is not None:
            existing[key] = value

    users_db[int(intent.metadata["path_params"]["user_id"])] = existing
    return {"status": "updated", "user": existing}

on(app, UPDATE_USER, handle_update_user)

@controller Style

from pydantic import BaseModel, Field
from evoid.web.controller import Service, Controller, PATCH

class SettingsUpdate(BaseModel):
    theme: str | None = None
    language: str | None = None
    notifications: bool | None = None

# Simulated settings
settings_db = {"theme": "light", "language": "en", "notifications": True}

app = Service("api")

@Controller("/settings")
class SettingsController:
    @PATCH("/")
    async def update_settings(self, updates: SettingsUpdate):
        # Merge updates
        update_data = updates.model_dump(exclude_unset=True)
        settings_db.update(update_data)
        
        return {"status": "updated", "settings": settings_db}

Summary

PatternUse Case
exclude_unset=TrueOnly include explicitly provided fields
None defaultsMake fields optional in updates
Processor mergeCentralized merge logic
Deep mergeNested object updates