Skip to content

Handling Request Inputs

Read and validate incoming request data in sillo (JSON bodies, form data, uploaded files, and streaming bodies) with the Request object and Pydantic models.

sillo gives every handler a HttpContext object (the first parameter) that lazily parses the incoming body the moment you ask for it. This guide covers the four input shapes you’ll handle: JSON, form data, uploaded files, and raw/streaming bodies, plus how to validate them with Pydantic.

from sillo import SilloApp, HttpContext
app = SilloApp()
@app.post("/submit")
async def submit_data(ctx: HttpContext):
data = await ctx.json # parse the body as JSON
return {"received": data}

ctx.json is an awaitable property, await it once and the body is cached for the rest of the request. It runs json.loads on the raw bytes, so it works for any body that is valid JSON regardless of the Content-Type header.

from sillo import HttpContext
@app.post("/submit")
async def submit_data(ctx: HttpContext):
data = await ctx.json
name = data.get("name")
return {"hello": name}

Common accessors:

  • await ctx.json: parsed JSON as a dict/list (raises on invalid JSON).
  • await ctx.text: the raw body decoded as text (UTF-8, falling back to latin-1).
  • await ctx.body: the raw body as bytes.

sillo parses both application/x-www-form-urlencoded and multipart/form-data into a FormData object, accessible via ctx.form (awaitable property) or the ctx.form_data context manager.

from sillo import HttpContext
@app.post("/submit-form")
async def submit_form(ctx: HttpContext):
form = await ctx.form # FormData object
username = form.get("username")
return {"received": username}

For forms, use ctx.form. For multipart uploads (files), read on below.

Uploaded files ride along inside multipart/form-data. Access them through ctx.files (awaitable property), which returns a dict of UploadedFile objects keyed by field name.

from sillo import HttpContext, json
@app.post("/upload")
async def upload_file(ctx: HttpContext):
files = await ctx.files
document = files.get("document")
if document is None:
return json({"error": "no file"}, status_code=400)
content = await document.read() # bytes
filename = document.filename
return {"saved": filename, "bytes": len(content)}

UploadedFile exposes filename, content_type, and an async read() coroutine. Always await ctx.files (and document.read()). Both are async.

For very large uploads you can consume the body in chunks instead of buffering it all. ctx.stream is an async generator of bytes:

from sillo import HttpContext
@app.post("/stream")
async def stream_data(ctx: HttpContext):
total = 0
async for chunk in ctx.stream: # async generator, NOT a method call
total += len(chunk)
return {"bytes_received": total}

Because the body is consumed as you iterate, you cannot also call await ctx.json or await ctx.form on the same request afterward. Pick one strategy per request.

For structured input, validate with a Pydantic v2 model. Parse the JSON yourself, then construct the model and let Pydantic handle coercion and errors.

from pydantic import BaseModel, EmailStr, ValidationError
from sillo import HttpContext, json
class UserSchema(BaseModel):
name: str
email: EmailStr
@app.post("/create-user")
async def create_user(ctx: HttpContext):
try:
payload = await ctx.json
user = UserSchema(**payload)
except ValidationError as e:
return json({"error": e.errors()}, status_code=422)
return {"user": user.model_dump()}

sillo also ships the request_model hook on routes for automatic validation. See Request Parameters and the dependency injection guides for that pattern.