diff --git a/ninja/openapi/schema.py b/ninja/openapi/schema.py index 494e4e1d8..44722ad10 100644 --- a/ninja/openapi/schema.py +++ b/ninja/openapi/schema.py @@ -236,7 +236,7 @@ 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"] @@ -244,7 +244,9 @@ def _create_multipart_schema_from_models( # 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 ] ) @@ -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 { diff --git a/tests/test_alias.py b/tests/test_alias.py index 3430ed6b4..b07cee87f 100644 --- a/tests/test_alias.py +++ b/tests/test_alias.py @@ -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): @@ -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