diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 227ad837d..d8bd81bbf 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -93,7 +93,18 @@ if PYDANTIC_V2: @property def alias(self) -> str: - a = self.field_info.alias + if ( + self.mode == "validation" + and self.field_info.validation_alias is not None + ): + a = self.field_info.validation_alias + elif ( + self.mode == "serialization" + and self.field_info.serialization_alias is not None + ): + a = self.field_info.serialization_alias + else: + a = self.field_info.alias return a if a is not None else self.name @property diff --git a/tests/test_compat.py b/tests/test_compat.py index f4a3093c5..84082469c 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -26,6 +26,61 @@ def test_model_field_default_required(): assert field.default is Undefined +@needs_pydanticv2 +def test_model_field_alias(): + field_info = FieldInfo(annotation=str, alias="foo") + field = ModelField(name="bar", field_info=field_info) + assert field.alias == "foo" + + +@needs_pydanticv2 +def test_model_field_serialization_alias(): + field_info = FieldInfo(annotation=str, serialization_alias="foo") + + field = ModelField(name="bar", field_info=field_info, mode="validation") + assert field.alias == "bar" + + field = ModelField(name="bar", field_info=field_info, mode="serialization") + assert field.alias == "foo" + + +@needs_pydanticv2 +def test_model_field_validation_alias(): + field_info = FieldInfo(annotation=str, validation_alias="foo") + + field = ModelField(name="bar", field_info=field_info, mode="validation") + assert field.alias == "foo" + + field = ModelField(name="bar", field_info=field_info, mode="serialization") + assert field.alias == "bar" + + +@needs_pydanticv2 +def test_model_field_both_specialized_alias(): + field_info = FieldInfo( + annotation=str, validation_alias="bar", serialization_alias="baz" + ) + + field = ModelField(name="foo", field_info=field_info, mode="validation") + assert field.alias == "bar" + + field = ModelField(name="foo", field_info=field_info, mode="serialization") + assert field.alias == "baz" + + +@needs_pydanticv2 +def test_model_field_alias_and_both_specialized_alias(): + field_info = FieldInfo( + annotation=str, alias="bar", validation_alias="baz", serialization_alias="qux" + ) + + field = ModelField(name="foo", field_info=field_info, mode="validation") + assert field.alias == "baz" + + field = ModelField(name="foo", field_info=field_info, mode="serialization") + assert field.alias == "qux" + + @needs_pydanticv1 def test_upload_file_dummy_with_info_plain_validator_function(): # For coverage diff --git a/tests/test_openapi_name_for_fields.py b/tests/test_openapi_name_for_fields.py new file mode 100644 index 000000000..c2c9a12b3 --- /dev/null +++ b/tests/test_openapi_name_for_fields.py @@ -0,0 +1,208 @@ +from typing import Annotated + +from fastapi import FastAPI, Query +from fastapi.testclient import TestClient +from pydantic import BaseModel, Field + +app = FastAPI() +client = TestClient(app) + + +class DataModelWithoutAlias(BaseModel): + foo: str = Field(...) + + +@app.get( + "/without-alias", + response_model=DataModelWithoutAlias, +) +def without_alias(data: Annotated[DataModelWithoutAlias, Query(...)]): + return data + + +def test_openapi_param_name_without_alias(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + openapi_schema = response.json() + + assert ( + openapi_schema["paths"]["/without-alias"]["get"]["parameters"][0]["name"] + == "foo" + ) + assert ( + openapi_schema["paths"]["/without-alias"]["get"]["parameters"][0]["in"] + == "query" + ) + assert openapi_schema["components"]["schemas"]["DataModelWithoutAlias"][ + "properties" + ]["foo"] == {"title": "Foo", "type": "string"} + + +class DataModelWithAlias(BaseModel): + foo: str = Field(..., alias="bar") + + +@app.get( + "/with-alias", + response_model=DataModelWithAlias, +) +def with_alias(data: Annotated[DataModelWithAlias, Query(...)]): + return data + + +def test_openapi_param_name_with_alias(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + openapi_schema = response.json() + + assert ( + openapi_schema["paths"]["/with-alias"]["get"]["parameters"][0]["name"] == "bar" + ) + assert ( + openapi_schema["paths"]["/with-alias"]["get"]["parameters"][0]["in"] == "query" + ) + assert openapi_schema["components"]["schemas"]["DataModelWithAlias"]["properties"][ + "bar" + ] == {"title": "Bar", "type": "string"} + + +class DataModelWithSerializationAlias(BaseModel): + foo: str = Field(..., serialization_alias="bar") + + +@app.get( + "/with-serialization-alias", + response_model=DataModelWithSerializationAlias, +) +def with_serialization_alias( + data: Annotated[DataModelWithSerializationAlias, Query(...)], +): + return data + + +def test_openapi_param_name_with_serialization_alias(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + openapi_schema = response.json() + + assert ( + openapi_schema["paths"]["/with-serialization-alias"]["get"]["parameters"][0][ + "name" + ] + == "foo" + ) + assert ( + openapi_schema["paths"]["/with-serialization-alias"]["get"]["parameters"][0][ + "in" + ] + == "query" + ) + assert openapi_schema["components"]["schemas"]["DataModelWithSerializationAlias"][ + "properties" + ]["bar"] == {"title": "Bar", "type": "string"} + + +class DataModelWithValidationAlias(BaseModel): + foo: str = Field(..., validation_alias="bar") + + +@app.get( + "/with-validation-alias", + response_model=DataModelWithValidationAlias, +) +def with_validation_alias(data: Annotated[DataModelWithValidationAlias, Query(...)]): + return data + + +def test_openapi_param_name_with_validation_alias(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + openapi_schema = response.json() + + assert ( + openapi_schema["paths"]["/with-validation-alias"]["get"]["parameters"][0][ + "name" + ] + == "bar" + ) + assert ( + openapi_schema["paths"]["/with-validation-alias"]["get"]["parameters"][0]["in"] + == "query" + ) + assert openapi_schema["components"]["schemas"]["DataModelWithValidationAlias"][ + "properties" + ]["foo"] == {"title": "Foo", "type": "string"} + + +class DataModelWithBothSpecializedAlias(BaseModel): + foo: str = Field(..., validation_alias="bar", serialization_alias="baz") + + +@app.get( + "/with-both-specialized-alias", + response_model=DataModelWithBothSpecializedAlias, +) +def with_both_specialized_alias( + data: Annotated[DataModelWithBothSpecializedAlias, Query(...)], +): + return data + + +def test_openapi_param_name_with_both_specialized_alias(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + openapi_schema = response.json() + + assert ( + openapi_schema["paths"]["/with-both-specialized-alias"]["get"]["parameters"][0][ + "name" + ] + == "bar" + ) + assert ( + openapi_schema["paths"]["/with-both-specialized-alias"]["get"]["parameters"][0][ + "in" + ] + == "query" + ) + assert openapi_schema["components"]["schemas"]["DataModelWithBothSpecializedAlias"][ + "properties" + ]["baz"] == {"title": "Baz", "type": "string"} + + +class DataModelWithAliasAndBothSpecializedAlias(BaseModel): + foo: str = Field( + ..., alias="bar", validation_alias="baz", serialization_alias="qux" + ) + + +@app.get( + "/with-alias-and-both-specialized-alias", + response_model=DataModelWithAliasAndBothSpecializedAlias, +) +def with_alias_and_both_specialized_alias( + data: Annotated[DataModelWithAliasAndBothSpecializedAlias, Query(...)], +): + return data + + +def test_openapi_param_name_with_alias_and_both_specialized_alias(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + openapi_schema = response.json() + + assert ( + openapi_schema["paths"]["/with-alias-and-both-specialized-alias"]["get"][ + "parameters" + ][0]["name"] + == "baz" + ) + assert ( + openapi_schema["paths"]["/with-alias-and-both-specialized-alias"]["get"][ + "parameters" + ][0]["in"] + == "query" + ) + assert openapi_schema["components"]["schemas"][ + "DataModelWithAliasAndBothSpecializedAlias" + ]["properties"]["qux"] == {"title": "Qux", "type": "string"}