Skip to content
Guide

Response Contracts

Validate and filter outgoing data with explicit public models and safe server-side errors.

from dataclasses import dataclass
from flasgo import Flasgo
app = Flasgo()
@dataclass
class PublicUser:
id: int
display_name: str
@app.get("/user", response_model=PublicUser, public=True)
def user() -> dict[str, object]:
return {"id": 1, "display_name": "Alice", "internal_note": "not part of the public response"}

The response contains only id and display_name. Filtering applies recursively to dataclasses and typed containers. Output types, required fields, finite numbers, and bounded validation are enforced before sending. Dataclass models can project mappings or other dataclass instances without constructing the output dataclass. Underscore-prefixed fields remain private and are omitted from output schemas. Ordinary mapping keys remain application data; use explicit public dataclasses when fields must be filtered. Typed mappings require str or Any key annotations, and actual output keys must always be strings. Unsupported key annotations, including nested mappings, fail at route registration or stream construction. Any intentionally permits arbitrary bounded JSON-compatible data.

Invalid output produces a safe 500 through ResponseValidationError, rather than reflecting rejected values as a request-validation error. Custom exception handlers must preserve that confidentiality. Tuple return values retain their status and headers. A raw Response cannot bypass a declared response contract: raise HTTPException for an error response, or use a separate uncontracted route where raw responses are required. Trusted application middleware can still modify responses and requires its own review.

Only response_model= enables runtime enforcement. Existing return annotations continue to describe OpenAPI without changing existing response coercion. Routes with no response model retain their current behavior.

The declared response_model also supplies the OpenAPI response schema. Unsupported models are rejected during registration, including unsupported nested field types. A top-level tuple model such as tuple[int, int] raises TypeError because a returned tuple is read as (body, status); use a list or a dataclass instead. Request validation and response validation are separate: a malformed request can return 422, while invalid application output is a server error.

def test_public_user() -> None:
response = app.test_client().get("/user")
assert response.status_code == 200
assert response.json() == {"id": 1, "display_name": "Alice"}

Normal output validation uses the application’s MAX_VALIDATION_DEPTH, MAX_VALIDATION_WORK, and MAX_VALIDATION_ISSUES settings. Streaming SSE and NDJSON use separate bounded per-item budgets and item_model=; see Streaming Responses.

Review field selection alongside object-level authorization. A correctly filtered response can still disclose another user’s record if the application selects the wrong object.