Response Envelopes and List Metadata#
Restly returns bare objects and a data envelope for lists; this page covers
changing the container around the data, not the fields inside it.
To change which fields an object exposes, see Custom Schemas and Field Types.
For a different schema per route (list vs detail), see Customize RestView.
To change the error shape, see Shape Error Responses.
What Restly returns by default#
Before changing the container, it helps to know what the generated routes put on the wire:
Route |
Response body |
|---|---|
|
The bare object, serialized through |
|
A |
|
|
The list envelope#
List endpoints paginate by default: the route wraps the page of objects in a
data envelope and adds pagination metadata.
{
"data": [ /* page of UserRead */ ],
"total_count": 123,
"page": 2,
"page_size": 50,
"total_pages": 3
}
When the client omits ?page_size=, the endpoint uses
default_page_size
(50). An explicit page_size may be smaller or larger than that default;
max_page_size is the
ceiling above which the request is rejected with 422.
To return every matching row uncapped, set
paginated to False. The
data envelope stays, but the count and page fields drop away:
@fr.include_view(app)
class TagView(fr.AsyncRestView):
prefix = "/tags"
model = Tag
schema = TagRead
paginated = False
# Response: {"data": [ /* every TagRead */ ]}
Restly keeps response_model and OpenAPI in sync with the envelope
automatically: the list route’s response annotation is a generated
PaginatedEnvelope wrapping the
response schema (a plain Envelope when
paginated is false), so no route-shell code is needed.
For how clients request pages (the page and page_size inputs), see
Pagination in the query-modifiers
guide.
Custom envelopes#
Any other envelope is a change to the HTTP contract, so
replace the endpoint method
and set response_model on the replacement. Inside the shell, call
to_response_schema(obj)
so that WriteOnly stripping, relationship-id resolution, and
response-schema validation still run.
For a single-object {"data": ...} wrapper, replace
get_one_endpoint and
create_endpoint:
import pydantic
class UserEnvelope(pydantic.BaseModel):
data: UserRead
@fr.include_view(app)
class UserView(fr.AsyncRestView):
prefix = "/users"
model = User
schema = UserRead
@fr.get("/{id}", response_model=UserEnvelope)
async def get_one_endpoint(self, id: int):
obj = await self.handle_get_one(id)
return {"data": self.to_response_schema(obj)}
@fr.post("/", response_model=UserEnvelope)
async def create_endpoint(self, schema_obj):
obj = await self.handle_create(schema_obj)
return {"data": self.to_response_schema(obj)}
For a list {"data": ..., "meta": ...} wrapper, replace
get_many_endpoint.
Keep the query_params parameter, which Restly annotates with the generated
filter, sort, and pagination query parameters, and reshape the default envelope
from
to_listing_response()
rather than redoing the page math by hand:
import pydantic
class PageMeta(pydantic.BaseModel):
total_count: int
page: int
page_size: int
total_pages: int
class UserListEnvelope(pydantic.BaseModel):
data: list[UserRead]
meta: PageMeta
@fr.include_view(app)
class UserView(fr.AsyncRestView):
prefix = "/users"
model = User
schema = UserRead
default_page_size = 50
@fr.get("/", response_model=UserListEnvelope)
async def get_many_endpoint(self, query_params):
result = await self.handle_get_many(query_params)
page = self.to_listing_response(query_params, result)
return {
"data": page["data"],
"meta": {
"total_count": page["total_count"],
"page": page["page"],
"page_size": page["page_size"],
"total_pages": page["total_pages"],
},
}
Envelope several routes at once: to_response#
When the same wrapper applies to more than one route, centralize it in
to_response() and have
each replaced shell delegate to it. to_response is the shared runtime
boundary keyed on the wire shape:
SINGLE,
LISTING, or
EMPTY. Its place among the
override points is covered in
Customize RestView.
def to_response(self, obj_or_list, shape=fr.ResponseShape.SINGLE):
if shape is fr.ResponseShape.SINGLE:
return {"data": self.to_response_schema(obj_or_list)}
return super().to_response(obj_or_list, shape)
Be aware that overriding to_response without also replacing the shells
leaves the generated shells’ response_model describing the bare object, so
FastAPI response validation and OpenAPI disagree with the enveloped payload
you return. A new contract therefore needs both pieces: the to_response
override for the runtime shape, and a replaced shell with a matching
response_model.
See also#
Custom Schemas and Field Types: which fields an object exposes.
Customize RestView: endpoint-method replacement mechanics, and a different schema for the list endpoint.
Shape Error Responses: errors bypass
to_response.Filter, Sort, and Paginate Lists: the pagination inputs clients send.
Relationship Loading and Async: how nested relationship fields load, and
MissingGreenleton async.