Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support serializing with the by_alias arg set #10

Merged
merged 7 commits into from
Aug 3, 2023
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
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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ljodal I added a test case as well, if you want to have a look at it.

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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to this name to clearly illustrate the difference between snake case and camel case.



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