Recommended resources
The freely available resources this lesson will direct you to.
Hello! Your request models now check incoming incident data, including meaningful text and the rule that high-severity incidents need a description. But a valid request does not guarantee a safe response. The demonstration POST handler currently returns a wrapper around the validated input; a real handler might return a stored record containing fields intended only for internal use.
This lesson adds an explicit public response model. You’ll use it to control which fields leave the API, inspect the resulting JSON and documentation, and distinguish response validation from request validation. Allow about 40 minutes for the short video, reading, implementation, and checks.
Treat the response as a separate contract
An incident can have several useful representations. The creation request describes what a client may submit. An internal record might also contain routing information or private triage notes. The response describes what a client may receive. Those sets of fields should not be assumed to be identical.
Watch response_model is declared for a single item and for a collection. The video reuses one model for input and output; that works for its simple example, but we need different models when the internal representation contains fields clients must not see.
Python FastAPI Tutorial: Build a REST API in 15 Minutes
In Python FastAPI Tutorial: Build a REST API in 15 Minutes, pixegami shows how a route declares the shape of its output. Watch for the decorator argument that controls the response.
Watch response models, from the shift away from request bodies to the examples for an item and a list. Focus on the route decorator, rather than copying the video's choice to use the same model on both sides.
The FastAPI documentation makes the security reason for separate models explicit. Read its input/output example and the later warning about using field-exclusion shortcuts as a public contract.
Read FastAPI’s Response Model - Return Type to see how an output model filters fields returned by a handler, and why a dedicated model is preferable to hiding selected fields on a larger model.
In “Add an output model,” begin at separate models and continue through the explanation immediately before “Response Model or Return Type.” Focus on what the handler returns versus what the client receives. Then, under “Response Model encoding parameters,” find “response_model_include and response_model_exclude.” Read from the shortcut warning, ending with the explanation that OpenAPI still describes the complete model.
A useful rule is to make the public response model an allowlist: declare the fields clients should receive, rather than maintaining a list of fields to remove. If another internal attribute is added later, it will not become public merely because someone forgot to update an exclusion list.
Define the public incident shape
Continue in src/incident_api/schemas.py. Keep IncidentCreate, IncidentUpdate, their validators, and your existing severity enum. Add a separate model alongside them:
class IncidentPublic(BaseModel):
id: int
title: str
severity: IncidentSeverity
description: str | None
Use your existing enum name if it differs from IncidentSeverity. This example exposes an assigned ID and the incident details the client needs, but no routing token or internal notes.
There is a small but important distinction in description: str | None: this response field is required but nullable. Every successful response represented by IncidentPublic must have a description key; its value may be null. That makes the response predictable even when a creation request omitted the description. In the previous lesson, the create model supplied a default of None, so its normal model_dump() includes that field.
The boundary is visible in FastAPI’s documentation image below. Its example request contains secret_name, while the documented successful response contains only the selected output fields. Our incident model follows the same principle, although its fields are different.

Apply the model to the POST handler
Replace the existing demonstration POST handler, rather than registering a second handler for the same route. Keep your existing app instance and PATCH handler. The following shows the imports and POST handler in context; adapt the import if your app file uses a different location:
from incident_api.schemas import IncidentCreate, IncidentPublic
@app.post("/incidents", response_model=IncidentPublic, status_code=201)
def create_incident(incident: IncidentCreate) -> dict[str, object]:
record: dict[str, object] = {
**incident.model_dump(),
"id": 42,
"internal_notes": "Internal triage only",
"routing_token": "example-only",
}
return record
This is still a demonstration, not persistence: ID 42 and the internal values are placeholders. In a real implementation, the handler would obtain a newly created record from the application’s storage path.
Trace the two boundaries. IncidentCreate validates the incoming body before the handler runs. The handler then returns a dictionary containing both public and internal fields. FastAPI uses response_model=IncidentPublic to validate and serialize the outgoing data, retaining only the declared response fields. The dict[str, object] return annotation accurately describes what this demonstration function returns internally; the decorator declares the HTTP response contract.
The earlier handler returned something like {"validated_input": ...}. That wrapper does not match IncidentPublic: it has no top-level id, title, or severity. Replacing it matters. A response model cannot manufacture required public fields from an incompatible return value. If required response data is missing or invalid, that is a server-side response error—not an incoming-body 422 that the client can fix.
Do not add response_model=IncidentPublic to the current PATCH demonstration just to make its output look consistent. That handler returns proposed changes, not a retrieved and updated incident. Once it actually produces a complete updated incident, it can use the same public response model.
Check both the wire response and the documentation
With the development server running, send a valid creation request:
curl -i -X POST 'http://127.0.0.1:8000/incidents' \
-H 'Content-Type: application/json' \
-d '{"title":" Deployment fails ","severity":"high","description":"Alerts firing"}'
Expect 201 Created and a JSON body shaped like this:
{
"id": 42,
"title": "Deployment fails",
"severity": "high",
"description": "Alerts firing"
}
The title is trimmed by the request validator you added previously. More importantly, neither internal_notes nor routing_token appears, despite being present in the handler’s returned dictionary. Open /docs and compare the POST operation’s request and successful-response schemas: the response should describe IncidentPublic, not the internal dictionary.
If you later return multiple incidents, the corresponding public contract can be declared as response_model=list[IncidentPublic]. Each item then needs the same explicit public shape. For now, the POST endpoint is enough to establish and verify the boundary.
A response model protects responses that FastAPI processes through that model; it is not a promise that sensitive data cannot leak anywhere. Avoid separately returning internal values in another route, placing them in an error detail, or bypassing model processing with a manually constructed response. The next lesson will address client errors; its error bodies need their own careful choices about what to reveal.
Takeaways
IncidentCreate governs what clients may send; IncidentPublic governs what successful responses may show. Declaring response_model=IncidentPublic makes FastAPI filter and validate the handler’s output and generates a matching response schema. A dedicated, small public model is clearer and safer than excluding sensitive fields one at a time from an internal model.
Next, you’ll handle missing incidents and invalid operations with consistent client errors, while keeping those error responses as deliberate as your successful ones.