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

respect the 'by_alias' route config also in generated docs for 'json body' data #1193

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
14 changes: 10 additions & 4 deletions ninja/openapi/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,15 +236,17 @@ def _create_schema_from_model(
return schema, True

def _create_multipart_schema_from_models(
self, models: TModels
self, models: TModels, by_alias: bool = True
) -> Tuple[DictStrAny, str]:
# We have File and Form or Body, so we need to use multipart (File)
content_type = BODY_CONTENT_TYPES["file"]

# get the various schemas
result = merge_schemas(
[
self._create_schema_from_model(model, remove_level=False)[0]
self._create_schema_from_model(
model, remove_level=False, by_alias=by_alias
)[0]
for model in models
]
)
Expand All @@ -265,10 +267,14 @@ def request_body(self, operation: Operation) -> DictStrAny:
model = models[0]
content_type = BODY_CONTENT_TYPES[model.__ninja_param_source__]
schema, required = self._create_schema_from_model(
model, remove_level=model.__ninja_param_source__ == "body"
model,
remove_level=model.__ninja_param_source__ == "body",
by_alias=operation.by_alias,
)
else:
schema, content_type = self._create_multipart_schema_from_models(models)
schema, content_type = self._create_multipart_schema_from_models(
models, by_alias=operation.by_alias
)
required = True

return {
Expand Down
102 changes: 102 additions & 0 deletions tests/test_alias.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from pydantic import ConfigDict
from pydantic.alias_generators import to_camel

from ninja import Field, NinjaAPI, Schema
from ninja.testing import TestClient


class SchemaWithAlias(Schema):
Expand Down Expand Up @@ -29,6 +33,104 @@ def test_alias():
}


# Make sure that both "runtime" (request/response) AND the generated openapi schemas respect the alias generator when controlling it with the "by_alias" parameter
# Before, the request body was not respected when generating the openapi schema.


class SchemaBodyWithAliasGenerator(Schema):
model_config = ConfigDict(
alias_generator=to_camel,
populate_by_name=True,
)
foo_bar: str = Field("")


class SchemaResponseWithAliasGenerator(Schema):
model_config = ConfigDict(
alias_generator=to_camel,
populate_by_name=True,
)
foo_bar: str = Field("")


api_without_alias = NinjaAPI()


@api_without_alias.post(
"/path-without-alias", response=SchemaResponseWithAliasGenerator, by_alias=False
)
def alias_operation_without_alias(request, payload: SchemaBodyWithAliasGenerator):
return {"foo_bar": payload.foo_bar}


def test_response_and_body_without_alias():
client = TestClient(api_without_alias)
assert client.post(
"/path-without-alias", json={"foo_bar": "foo_bar indeed"}
).json() == {"foo_bar": "foo_bar indeed"}

schema = api_without_alias.get_openapi_schema()["components"]
print(schema)

assert schema == {
"schemas": {
"SchemaBodyWithAliasGenerator": {
"type": "object",
"properties": {
"foo_bar": {"type": "string", "default": "", "title": "Foo Bar"}
},
"title": "SchemaBodyWithAliasGenerator",
},
"SchemaResponseWithAliasGenerator": {
"type": "object",
"properties": {
"foo_bar": {"type": "string", "default": "", "title": "Foo Bar"}
},
"title": "SchemaResponseWithAliasGenerator",
},
}
}


api_with_alias = NinjaAPI()


@api_with_alias.post(
"/path-with-alias", response=SchemaResponseWithAliasGenerator, by_alias=True
)
def alias_operation_with_alias(request, payload: SchemaBodyWithAliasGenerator):
return {"foo_bar": payload.foo_bar}


def test_response_and_body_with_alias():
client = TestClient(api_with_alias)
assert client.post("/path-with-alias", json={"fooBar": "fooBar indeed"}).json() == {
"fooBar": "fooBar indeed"
}

schema = api_with_alias.get_openapi_schema()["components"]
print(schema)

assert schema == {
"schemas": {
"SchemaBodyWithAliasGenerator": {
"type": "object",
"properties": {
"fooBar": {"type": "string", "default": "", "title": "Foobar"}
},
"title": "SchemaBodyWithAliasGenerator",
},
"SchemaResponseWithAliasGenerator": {
"type": "object",
"properties": {
"fooBar": {"type": "string", "default": "", "title": "Foobar"}
},
"title": "SchemaResponseWithAliasGenerator",
},
}
}


# TODO: check the conflicting approach
# when alias is used both for response and request schema
# basically it need to generate 2 schemas - one with alias another without
Expand Down
Loading