Unit 10.01: Validating the request before the model sees it
Validation belongs on the request model, where it runs before any of your code.
Length bounds and a content rule
A field validator that rejects card numbers alongside length bounds.
The code posts three payloads.
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field, field_validator
app = FastAPI()
class Ask(BaseModel):
question: str = Field(min_length=1, max_length=4000)
@field_validator("question")
@classmethod
def no_card_numbers(cls, v):
digits = "".join(ch for ch in v if ch.isdigit())
if len(digits) >= 16:
raise ValueError("looks like it contains a card number")
return v
@app.post("/ask")
def ask(body: Ask):
return {"answer": "ok"}
client = TestClient(app)
for payload in [{"question": "How long?"}, {"question": ""},
{"question": "my card is 4111 1111 1111 1111"}]:
r = client.post("/ask", json=payload)
print(f"{str(payload)[:44]:46} -> {r.status_code}")
# All three are rejected or accepted before the handler runs, so nothing
# invalid reaches the prompt, the provider, or your logs.
All three decisions happen before the handler body executes. The card number never reaches the prompt, the provider, or your logs - which is the same argument as Module 4, now enforced at the edge of the system.
The empty and oversized cases are rejected by declared bounds rather than by hand-written checks, so they cannot be forgotten in a new endpoint.
The mistake this prevents
The mistake is validating in the handler. It works for one endpoint, and the second endpoint gets a slightly different version of the same checks - which is how one path ends up without the card-number rule.
Takeaway
Put validation on the request model so it runs before your code and cannot be forgotten by a new endpoint.
