Skip to content

Commit

Permalink
Support serializing with the by_alias arg set (#10)
Browse files Browse the repository at this point in the history
Enables us to define custom aliases for the fields in the Pydantic model, and have the @api decorator serialize the model using those aliases.
  • Loading branch information
mortenkrane authored Aug 3, 2023
1 parent d62d890 commit 2a6846b
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 14 deletions.
7 changes: 6 additions & 1 deletion django_api_decorator/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def api(
response_status: int = 200,
atomic: bool | None = None,
auth_check: Callable[[HttpRequest], bool] | None = None,
serialize_by_alias: bool = False,
) -> Callable[[Callable[P, T]], Callable[P, HttpResponse]]:
"""
Defines an API view. This handles validation of query parameters, parsing of
Expand All @@ -49,6 +50,10 @@ def api(
HTTP status code to use if the view _does not_ return an
Response object, but rather just the data we should return.
* serialize_by_alias:
Is passed as the by_alias argument to TypeAdapter.dump_json(), making
the model use the aliases defined in model_config when serializing.
The request body parsing is done by inspecting the view parameter types. If
the view has a body parameter, we will try to decode the payload to that
type. Currently Django Rest Framework serializers and pydantic models are are
Expand Down Expand Up @@ -182,7 +187,7 @@ def inner(request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
)

# Encode the response from the view to json and create a response object.
payload = response_adapter.dump_json(response)
payload = response_adapter.dump_json(response, by_alias=serialize_by_alias)
return HttpResponse(
payload, status=response_status, content_type="application/json"
)
Expand Down
84 changes: 71 additions & 13 deletions tests/test_response_encoding.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,51 @@
import random
import re

import pytest
from django.http import HttpRequest, JsonResponse
from django.test.client import Client
from django.urls import path
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from typing_extensions import TypedDict

from django_api_decorator.decorators import api
from django_api_decorator.openapi import generate_api_spec


class MyTypedDict(TypedDict):
a: int
an_integer: int


class MyPydanticModel(BaseModel):
a: int
an_integer: int


def camel_case(string: str) -> str:
"""
Convert string from snake_case to camelCase
"""

_pascal_case = re.sub(r"(?:^|_)(.)", lambda m: m.group(1).upper(), string)
return _pascal_case[0].lower() + _pascal_case[1:]


class MyCamelCasePydanticModel(BaseModel):
model_config = ConfigDict(
alias_generator=camel_case,
populate_by_name=True,
)

an_integer: int


@api(method="GET")
def view_json_response(r: HttpRequest) -> JsonResponse:
return JsonResponse({"a": 1})
return JsonResponse({"an_integer": 1})


@api(method="GET")
def view_typed_dict(r: HttpRequest) -> MyTypedDict:
return {"a": 1}
return {"an_integer": 1}


@api(method="GET")
Expand All @@ -41,7 +60,12 @@ def view_bool(r: HttpRequest) -> bool:

@api(method="GET")
def view_pydantic_model(r: HttpRequest) -> MyPydanticModel:
return MyPydanticModel(a=1)
return MyPydanticModel(an_integer=1)


@api(method="GET", serialize_by_alias=True)
def view_camel_case_pydantic_model(r: HttpRequest) -> MyCamelCasePydanticModel:
return MyCamelCasePydanticModel(an_integer=1)


@api(method="GET")
Expand All @@ -55,18 +79,20 @@ def view_union(r: HttpRequest) -> int | str:
path("int", view_int),
path("bool", view_bool),
path("pydantic-model", view_pydantic_model),
path("pydantic-camel-case-model", view_camel_case_pydantic_model),
path("union", view_union),
]


@pytest.mark.parametrize(
"url,expected_response",
[
("/json-response", b'{"a": 1}'),
("/typed-dict", b'{"a":1}'),
("/json-response", b'{"an_integer": 1}'),
("/typed-dict", b'{"an_integer":1}'),
("/int", b"1"),
("/bool", b"false"),
("/pydantic-model", b'{"a":1}'),
("/pydantic-model", b'{"an_integer":1}'),
("/pydantic-camel-case-model", b'{"anInteger":1}'),
],
)
@pytest.mark.urls(__name__)
Expand Down Expand Up @@ -105,6 +131,26 @@ def test_schema() -> None:
},
}
},
"/pydantic-camel-case-model": {
"get": {
"operationId": "view_camel_case_pydantic_model",
"description": "",
"tags": ["test_response_encoding"],
"parameters": [],
"responses": {
200: {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MyCamelCasePydanticModel" # noqa: E501
}
}
},
}
},
}
},
"/pydantic-model": {
"get": {
"operationId": "view_pydantic_model",
Expand Down Expand Up @@ -189,15 +235,27 @@ def test_schema() -> None:
},
"components": {
"schemas": {
"MyCamelCasePydanticModel": {
"properties": {
"anInteger": {"title": "Aninteger", "type": "integer"}
},
"required": ["anInteger"],
"title": "MyCamelCasePydanticModel",
"type": "object",
},
"MyPydanticModel": {
"properties": {"a": {"title": "A", "type": "integer"}},
"required": ["a"],
"properties": {
"an_integer": {"title": "An Integer", "type": "integer"}
},
"required": ["an_integer"],
"title": "MyPydanticModel",
"type": "object",
},
"MyTypedDict": {
"properties": {"a": {"title": "A", "type": "integer"}},
"required": ["a"],
"properties": {
"an_integer": {"title": "An Integer", "type": "integer"}
},
"required": ["an_integer"],
"title": "MyTypedDict",
"type": "object",
},
Expand Down

0 comments on commit 2a6846b

Please sign in to comment.