Validation¶
Use this when you want Korvex to read, check and convert the input of a handler from its annotations.
Annotations say where a parameter comes from, and what it must be.
Models can be msgspec structs,
pydantic models, or plain dataclasses and TypedDicts;
Korvex needs none of these libraries to import, only to validate.
import msgspec
from korvex import App
app = App()
class NewUser(msgspec.Struct):
name: str
age: int
class User(msgspec.Struct):
id: int
name: str
@app.post("/users", status=201)
def create_user(body: NewUser, notify: bool = False) -> User:
return User(id=1, name=body.name)
Where a parameter comes from¶
| Parameter | Comes from |
|---|---|
a model (struct, pydantic model, dataclass, TypedDict), or a list of them (body: NewUser) |
the JSON body |
named like a path parameter (id) |
the path |
str, int, float, bool, UUID, date, datetime, or an Optional or list of them |
the query string |
Header(), Cookie(), Form(), Query(alias=...) |
there |
Depends(func) |
what func returns: see Dependencies |
named request, or annotated Request |
the request itself |
- A default makes a parameter optional. Dates are ISO 8601.
- Anything else is refused with a message when the route is declared, so a mistake shows at import.
- When a field is repeated (
?tag=a&tag=b), a single value takes the first, and alist[str]takes them all.
Dataclasses and TypedDict¶
A @dataclass or a TypedDict (or a list or Optional of them) works like a
struct: body, return annotation and Form(), with the same 422. Defaults are
filled in. It is checked by msgspec if installed, else pydantic; with neither,
the route raises a TypeError telling you to pip install msgspec. A
NamedTuple or attrs class is not supported. Before Python 3.12, pydantic wants
TypedDict from typing_extensions.
from dataclasses import dataclass
from typing import TypedDict
from korvex import App
from korvex.testing import TestClient
app = App()
@dataclass
class NewTask:
title: str
done: bool = False
class Summary(TypedDict):
title: str
done: bool
@app.post("/tasks", status=201)
def add(task: NewTask) -> Summary:
return {"title": task.title, "done": task.done}
print(TestClient(app).post("/tasks", json={"title": "milk"}).json())
Headers, cookies and forms¶
from korvex import App, Cookie, Form, Header, Query, UploadedFile
app = App()
@app.post("/photos")
async def upload(
title: str = Form(), # a form field, urlencoded or multipart
photo: UploadedFile = Form(), # a file from a multipart form
x_client: str = Header("web"), # the X-Client header, "web" if missing
theme: str = Cookie("light"),
page_size: int = Query(20, alias="page-size"),
): ...
x_clientreadsX-Client: underscores become hyphens, and case doesn't matter.alias=gives the exact name.list[str]gets every value of a repeated header.- An empty form field counts as missing, except for a
str. - A whole model can come from a form, field by field:
signup: Signup = Form(). A checkbox'sonis true, and text converts to numbers and dates as the model says. - All four also work in
Annotated[...](fromtyping):theme: Annotated[str, Cookie()] = "light".
What the handler returns¶
- A model, with a return annotation (
-> User): sent as JSON, with the route'sstatus(200 by default). None, annotated-> None: an empty response, forstatus=204.- A
Response: sent as it is, with its own status.
Errors¶
Invalid input fails validation and gets a 422 listing the problems:
{"detail": [{"loc": ["body", "age"], "msg": "Expected `int`, got `str`", "type": "validation"}]}
loc says where: ["query", "page"], ["header", "x-client"],
["form", "title"]. pydantic lists every problem; msgspec stops at the
first. To answer differently, handle the 422; the exception is a
RequestValidationError, with the list in errors:
from korvex import App, RequestValidationError, Response
app = App()
@app.exception_handler(422)
def invalid(request, exc: RequestValidationError):
return Response.json({"problems": [e["loc"] for e in exc.errors]}, status=422)
Annotations are read once, when the route is declared.
Next: Requests and responses.