Routing & Response Wrapping
ZodiacCore enhances FastAPI's routing system with default response standardization. Body-bearing endpoints use a consistent JSON structure without manual boilerplate, while raw response objects and HTTP no-body response semantics remain available.
1. The Zodiac APIRouter
The APIRouter in ZodiacCore accepts FastAPI-style route declarations while adding a mandatory envelope for ordinary response data. It uses a custom ZodiacRoute class to wrap response models and returned values.
Automatic Wrapping
When you return a dictionary, a Pydantic model, or a list from your route, Zodiac automatically wraps it in a Response model:
from zodiac_core.routing import APIRouter
router = APIRouter()
@router.get("/status")
async def get_status():
return {"status": "online"}
Resulting JSON:
When response_model is omitted, Zodiac infers the payload type from the endpoint's return annotation. An endpoint without a return annotation uses Response[Any].
Response Model Semantics
| Route declaration | Runtime behavior | OpenAPI response model |
|---|---|---|
Omitted response_model, return type T |
Automatically wrapped | Response[T] |
Omitted response_model, no return type |
Automatically wrapped | Response[Any] |
response_model=T |
Automatically wrapped | Response[T] |
response_model=None, ordinary return value |
Automatically wrapped without constraining data |
Response[Any] |
response_model omitted or set to None, return type is a FastAPI/Starlette Response |
Raw response is passed through | No Zodiac envelope model |
Status code <200, 204, 205, or 304 |
No response body or envelope | No response content |
Declaring a non-empty response model for a status code that prohibits a body remains an error, matching FastAPI.
Unlike FastAPI's native router, Zodiac intentionally treats response_model=None as an untyped payload rather than as an envelope opt-out. This preserves the code, data, and message contract. Runtime passthrough is based on the actual returned FastAPI/Starlette Response object; its return annotation keeps OpenAPI aligned with that raw response.
2. Standard Response Structure
Body-bearing responses wrapped by Zodiac follow this schema:
| Field | Type | Description |
|---|---|---|
code |
int |
Business status code (0 for success). |
message |
string |
A brief description of the result. |
data |
any |
The actual payload (result of your function). |
Manual Responses
Raw FastAPI/Starlette Response objects, such as FileResponse and StreamingResponse, are passed through without runtime wrapping. Annotate the raw response return type so OpenAPI does not advertise a Zodiac envelope.
from fastapi import Response
@router.get("/custom")
async def manual() -> Response:
return Response("custom", media_type="text/plain")
Yield-Based Streaming
When the installed FastAPI provides native yield-based streaming, Zodiac leaves synchronous and asynchronous generator endpoints unchanged. Streamed chunks are not wrapped in the Zodiac Response envelope.
FastAPI 0.134.0 added yield-based JSON Lines (JSONL) and binary streaming. A generator using the default response class produces JSONL, while response_class=StreamingResponse sends raw strings or bytes:
from collections.abc import AsyncIterable, Iterator
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
@router.get("/users/stream")
async def stream_users() -> AsyncIterable[User]:
yield User(id=1, name="First")
yield User(id=2, name="Second")
@router.get("/download", response_class=StreamingResponse)
def stream_download() -> Iterator[bytes]:
yield b"first chunk\n"
yield b"second chunk\n"
FastAPI 0.135.0 added native Server-Sent Events (SSE). EventSourceResponse is a StreamingResponse subclass, so it receives the same passthrough behavior:
from collections.abc import AsyncIterable
from fastapi.sse import EventSourceResponse
@router.get("/events", response_class=EventSourceResponse)
async def stream_events() -> AsyncIterable[dict[str, str]]:
yield {"event": "ready"}
yield {"event": "complete"}
FastAPI versions before 0.134.0 do not provide native yield-based streaming, so Zodiac does not opt generator endpoints into passthrough on those versions. Unannotated generators retain the ordinary response-envelope behavior; iterator return annotations are still subject to the older FastAPI/Pydantic response-model limitations and may fail during route registration. For streaming that must also work on older FastAPI versions, explicitly return a StreamingResponse object instead. See FastAPI's guides for JSONL streaming, raw stream data, and SSE.
3. OpenAPI Integration
ZodiacCore's APIRouter dynamically generates Pydantic models for wrapped responses. Swagger UI (/docs) displays the code, message, and data fields, with data mapped to an explicit response model or the endpoint's return annotation. response_model=None produces Response[Any]; annotated raw responses and no-body status codes do not generate a Zodiac envelope schema.
4. API Reference
Routing Utilities
APIRouter
Bases: APIRouter
Zodiac-enhanced APIRouter that uses ZodiacRoute by default.
Body-bearing routes registered via this router will, by default: - Infer omitted response models from endpoint return annotations - Wrap response models with Response[T] for OpenAPI docs - Wrap endpoint return values with the Response structure
response_model=None keeps the envelope with an unconstrained Any payload. Return a FastAPI/Starlette Response object to bypass runtime wrapping. Generator endpoints and StreamingResponse classes retain FastAPI's native streaming behavior and are not wrapped in the Response structure.
Source code in zodiac_core/routing.py
ZodiacRoute
Bases: APIRoute
Custom APIRoute that wraps body-bearing response models and endpoint returns with the standard Response[T] structure by default.
Raw Response objects pass through at runtime, while status codes that prohibit a response body bypass automatic wrapping entirely.
Source code in zodiac_core/routing.py
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | |
Response Helpers
Response
Bases: BaseModel, Generic[T]
Standard API response model.
Source code in zodiac_core/response.py
create_response(http_code, code=None, data=None, message='')
Create a standardized JSON response.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
http_code
|
int
|
HTTP status code |
required |
code
|
Optional[int]
|
Business status code (defaults to http_code if not provided) |
None
|
data
|
Any
|
Response payload |
None
|
message
|
str
|
Response message |
''
|
Source code in zodiac_core/response.py
response_ok(code=None, data=None, message='Success')
Create a successful response (200 OK)
Source code in zodiac_core/response.py
response_created(code=None, data=None, message='Created')
Create a resource created response (201 Created)
Source code in zodiac_core/response.py
response_bad_request(code=None, data=None, message='Bad Request')
Create a bad request error response (400 Bad Request)
Source code in zodiac_core/response.py
response_unauthorized(code=None, data=None, message='Unauthorized')
Create an unauthorized response (401 Unauthorized)
Source code in zodiac_core/response.py
response_forbidden(code=None, data=None, message='Forbidden')
Create a forbidden response (403 Forbidden)
Source code in zodiac_core/response.py
response_not_found(code=None, data=None, message='Not Found')
Create a not found response (404 Not Found)
Source code in zodiac_core/response.py
response_conflict(code=None, data=None, message='Conflict')
Create a conflict response (409 Conflict)
Source code in zodiac_core/response.py
response_unprocessable_entity(code=None, data=None, message='Unprocessable Entity')
Create an unprocessable entity response (422 Unprocessable Entity)
Source code in zodiac_core/response.py
response_server_error(code=None, data=None, message='Internal Server Error')
Create a server error response (500 Internal Server Error)