Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions docs/techniques/response-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,73 @@ They include:
- `model_field_or_schema`: `Optional` property. For return data validation. Default: `None` **Optional**


## **Response Resolution Process**

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:

### **1. Determine the Status Code**

The status code is determined in the following priority order:

1. **Single Model Case**: If only one response model is defined, its status code is used as the default.
```python
@get("/item", response=UserSchema) # Defaults to status code 200
def get_item(self):
return dict(username='Ellar')
```

2. **Response Object Status Code**: If a Response object was created and has a status code set, that takes precedence.
```python
@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')
```

3. **Tuple Return Value**: If the handler returns a tuple of `(response_obj, status_code)`, the status code from the tuple is used.
```python
@get("/item", response={200: UserSchema, 201: UserSchema})
def get_item(self):
return dict(username='Ellar'), 201 # Returns with status code 201
```

### **2. Match to Response Model**

After determining the status code, Ellar matches it to the appropriate response model:

1. **Exact Match**: If a response model is defined for the specific status code, it's used.
2. **Ellipsis Fallback**: If no exact match is found but an `Ellipsis` (`...`) key exists, that model is used as a catch-all.
3. **Default Fallback**: If no match is found, `EmptyAPIResponseModel` is used with a warning logged.

Example with Ellipsis fallback:

```python
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), status
```

In 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.

## **Different Response Models**
Let's see different `ResponseModel` available in Ellar and how you can create one too.

Expand Down
2 changes: 1 addition & 1 deletion ellar/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Ellar - Python ASGI web framework for building fast, efficient, and scalable RESTful APIs and server-side applications."""

__version__ = "0.9.3"
__version__ = "0.9.4"
2 changes: 1 addition & 1 deletion ellar/common/responses/models/route.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def response_resolver(
status_code = http_connection.get_response().status_code

if isinstance(response_obj, tuple) and len(response_obj) == 2:
status_code, response_obj = endpoint_response_content
response_obj, status_code = endpoint_response_content

if status_code in self.models:
response_model = self.models[status_code]
Expand Down
12 changes: 6 additions & 6 deletions tests/test_response/test_defined_response_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,22 @@ def get_validlist():

@mr.get("/items/valid_tuple_return", response={200: List[Item], 201: Item})
def get_valid_tuple_return():
return 201, {"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}
return {"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, 201


@mr.get("/items/not_found_res_model", response={200: List[Item], 201: Item})
def get_not_found_res_model():
return 301, {"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}
return {"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, 301


@mr.get("/items/text-case-1", response=PlainTextResponse)
def get_plain_text_case_1():
return '301, {"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}'
return '{"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, 301'


@mr.get("/items/text-case-2", response={200: PlainTextResponse, 201: Item})
def get_plain_text_case_2():
return '301, {"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}'
return '{"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, 301'


@mr.get("/items/text-case-3", response={200: PlainTextResponse})
Expand Down Expand Up @@ -107,14 +107,14 @@ def test_plain_test_case_1(test_client_factory):
response = client.get("/items/text-case-1")
response.raise_for_status()
assert "text/plain" in str(response.headers["content-type"])
assert response.text == '301, {"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}'
assert response.text == '{"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, 301'


def test_plain_test_case_2(test_client_factory):
client = test_client_factory(app)
response = client.get("/items/text-case-2")
assert "text/plain" in str(response.headers["content-type"])
assert response.text == '301, {"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}'
assert response.text == '{"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, 301'


def test_sent_without_response_model_response(test_client_factory):
Expand Down
4 changes: 2 additions & 2 deletions tests/test_response/test_pydantic_response_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ def get_valid_ellipsis(switch: common.Query[str]):
Item(aliased_name="bar", price=1.0),
Item(aliased_name="bar2", price=2.0),
]
return 201, {
return {
"k1": ItemSerializer(aliased_name="foo"),
"k3": ItemSerializer(aliased_name="baz", price=2.0, owner_ids=[1, 2, 3]),
}
}, 201


app = AppFactory.create_app(routers=(mr,))
Expand Down