Each route handler has key-value pair of status codes and a response model. This response model holds information on the type of response to be returned.
# project_name/apps/items/controllers.py
from ellar.common import Controller, get, Serializer, ControllerBase
class UserSchema(Serializer):
username: str
email: str = None
first_name: str = None
last_name: str = None
@Controller
class ItemsController(ControllerBase):
@get("/me", response=UserSchema)
def me(self):
return dict(username='Ellar', email='ellar@example.com')During route response computation, the me route handler response will evaluate to a
JSONResponseModel with UserSchema as content validation schema.
The resulting route responses will be:
from ellar.common import Serializer
from ellar.common.responses.models import JSONResponseModel
class UserSchema(Serializer):
username: str
email: str = None
first_name: str = None
last_name: str = None
response = {200: JSONResponseModel(model_field_or_schema=UserSchema)}For documentation purposes, we can apply some description to the returned response
@get("/me", response=(UserSchema, 'User Schema Response'))
def me(self):
return dict(username='Ellar', email='ellar@example.com')This will be translated to:
response = {200: JSONResponseModel(model_field_or_schema=UserSchema, description='User Schema Response')}!!! info
Each route handler has its own ResponseModel computation and validation. If there is no response definition, Ellar default the route handler model to EmptyAPIResponseModel.
When you use a Response class as response, a ResponseModel is used and the response_type is replaced with applied response class.
For example:
# project_name/apps/items/controllers.py
from ellar.common import Controller, get, ControllerBase,PlainTextResponse, Serializer
class UserSchema(Serializer):
username: str
email: str = None
first_name: str = None
last_name: str = None
@Controller
class ItemsController(ControllerBase):
@get("/me", response={200: PlainTextResponse, 201: UserSchema})
def me(self):
return "some text response."This will be translated to:
from ellar.common.responses.models import ResponseModel, JSONResponseModel
from ellar.common import PlainTextResponse
response = {200: ResponseModel(response_type=PlainTextResponse), 201: JSONResponseModel(model_field_or_schema=UserSchema)}All response model follows IResponseModel contract.
import typing as t
from ellar.pydantic.fields import ModelField
from ellar.common import IExecutionContext, Response
class IResponseModel:
media_type: str
description: str
get_model_field: t.Callable[..., t.Optional[t.Union[ModelField, t.Any]]]
create_response: t.Callable[[IExecutionContext, t.Any], Response]Properties Overview:
media_type: Read from response media type. Requireddescription: For documentation purpose. Default:Success Response. Optionalget_model_field: returns response schema if any. Optionalcreate_response: returns a response for the client. Optional
There is also a BaseResponseModel concrete class for more generic implementation.
And its adds extra properties for configuration purposes.
They include:
response_type: Response classes eg. JSONResponse, PlainResponse, HTMLResponse. etc. Default:Response. Requiredmodel_field_or_schema:Optionalproperty. For return data validation. Default:NoneOptional
When a route handler returns a response, Ellar goes through a resolution process to determine which response model to use.
This process is handled by the response_resolver method and follows these steps:
The status code is determined in the following priority order:
-
Single Model Case: If only one response model is defined, its status code is used as the default.
@get("/item", response=UserSchema) # Defaults to status code 200 def get_item(self): return dict(username='Ellar')
-
Response Object Status Code: If a Response object was created and has a status code set, that takes precedence.
@get("/item", response={200: UserSchema, 201: UserSchema}) def get_item(self, res: Response): res.status_code = 201 # This status code will be used return dict(username='Ellar')
-
Tuple Return Value: If the handler returns a tuple of
(response_obj, status_code), the status code from the tuple is used.@get("/item", response={200: UserSchema, 201: UserSchema}) def get_item(self): return dict(username='Ellar'), 201 # Returns with status code 201
After determining the status code, Ellar matches it to the appropriate response model:
- Exact Match: If a response model is defined for the specific status code, it's used.
- Ellipsis Fallback: If no exact match is found but an
Ellipsis(...) key exists, that model is used as a catch-all. - Default Fallback: If no match is found,
EmptyAPIResponseModelis used with a warning logged.
Example with Ellipsis fallback:
from ellar.common import Controller, get, ControllerBase, Serializer
class UserSchema(Serializer):
username: str
email: str = None
class ErrorSchema(Serializer):
message: str
code: int
@Controller
class ItemsController(ControllerBase):
@get("/item", response={200: UserSchema, ...: ErrorSchema})
def get_item(self, status: int):
if status == 200:
return dict(username='Ellar', email='ellar@example.com')
# Any other status code will use ErrorSchema
return dict(message='Error occurred', code=status), statusIn this example, returning with status code 200 uses UserSchema, but any other status code (404, 500, etc.) will use ErrorSchema as the fallback.
!!! tip
Using the Ellipsis (...) key is useful when you want to define a catch-all response model for various status codes (like error responses) without defining each one explicitly.
Let's see different ResponseModel available in Ellar and how you can create one too.
Response model that manages rendering of other response types.
- Location:
ellar.common.responses.models.ResponseModel - response_type:
Response - model_field_or_schema:
None - media_type:
text/plain
Response model that manages JSON response.
- Location:
ellar.common.responses.models.json.JSONResponseModel - response_type:
JSONResponseORconfig.DEFAULT_JSON_CLASS - model_field_or_schema:
Required - media_type:
application/json
Response model that manages HTML templating response. see @render decorator.
- Location:
ellar.common.responses.models.html.HTMLResponseModel - response_type:
TemplateResponse - model_field_or_schema:
None - media_type:
text/html
Response model that manages FILE response. see @file decorator.
- Location:
ellar.common.responses.models.file.FileResponseModel - response_type:
FileResponse - model_field_or_schema:
ellar.common.responses.models.file.FileResponseModelSchema - media_type:
Required
Response model that manages STREAMING response. see @file decorator.
- Location:
ellar.common.responses.models.file.StreamingResponseModel - response_type:
StreamingResponse - model_field_or_schema:
ellar.common.responses.models.file.StreamResponseModelSchema - media_type:
Required
Default ResponseModel applied when no response is defined.
- Location:
ellar.common.responses.models.json.EmptyAPIResponseModel - response_type:
JSONResponseORconfig.DEFAULT_JSON_CLASS - model_field_or_schema:
dict - media_type:
application/json
Lets create a new JSON response model.
# project_name/apps/items/controllers.py
import typing as t
from ellar.common import Controller, get, ControllerBase, JSONResponse, Serializer
from ellar.common.responses.models import ResponseModel
class NoteSchema(Serializer):
id: t.Union[int, None]
text: str
completed: bool
class JsonApiResponse(JSONResponse):
media_type = "application/vnd.api+json"
class JsonApiResponseModel(ResponseModel):
response_type = JsonApiResponse
model_field_or_schema = t.List[NoteSchema]
default_description = 'Successful JsonAPI Response'
@Controller
class ItemsController(ControllerBase):
@get("/notes/", response=JsonApiResponseModel())
def get_notes(self):
return [
dict(id=1, text='My Json Api Response 1', completed=True),
dict(id=2, text='My Json Api Response 2', completed=True),
]
