From 785270acfc930d73fcab0d44348a09117bbe6764 Mon Sep 17 00:00:00 2001 From: Ravishankar Sivasubramaniam Date: Thu, 25 Sep 2025 15:53:32 -0500 Subject: [PATCH 01/15] feat: Add support for Pydantic v2 aliases --- sqlmodel/_compat.py | 9 ++- sqlmodel/main.py | 97 +++++++++++++++---------- tests/test_aliases.py | 161 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 229 insertions(+), 38 deletions(-) create mode 100644 tests/test_aliases.py diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 38dd501c4a..0ed19b925b 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -221,8 +221,13 @@ def get_field_metadata(field: Any) -> Any: return FakeMetadata() def post_init_field_info(field_info: FieldInfo) -> None: - return None - + if IS_PYDANTIC_V2: + if field_info.alias and not field_info.validation_alias: + field_info.validation_alias = field_info.alias + if field_info.alias and not field_info.serialization_alias: + field_info.serialization_alias = field_info.alias + else: + field_info._validate() # type: ignore[attr-defined] # Dummy to make it importable def _calculate_keys( self: "SQLModel", diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 38c85915aa..1578b030bf 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -215,6 +215,8 @@ def Field( *, default_factory: Optional[NoArgAnyCallable] = None, alias: Optional[str] = None, + validation_alias: Optional[str] = None, + serialization_alias: Optional[str] = None, title: Optional[str] = None, description: Optional[str] = None, exclude: Union[ @@ -260,6 +262,8 @@ def Field( *, default_factory: Optional[NoArgAnyCallable] = None, alias: Optional[str] = None, + validation_alias: Optional[str] = None, + serialization_alias: Optional[str] = None, title: Optional[str] = None, description: Optional[str] = None, exclude: Union[ @@ -314,6 +318,8 @@ def Field( *, default_factory: Optional[NoArgAnyCallable] = None, alias: Optional[str] = None, + validation_alias: Optional[str] = None, + serialization_alias: Optional[str] = None, title: Optional[str] = None, description: Optional[str] = None, exclude: Union[ @@ -349,6 +355,8 @@ def Field( *, default_factory: Optional[NoArgAnyCallable] = None, alias: Optional[str] = None, + validation_alias: Optional[str] = None, + serialization_alias: Optional[str] = None, title: Optional[str] = None, description: Optional[str] = None, exclude: Union[ @@ -387,43 +395,60 @@ def Field( schema_extra: Optional[Dict[str, Any]] = None, ) -> Any: current_schema_extra = schema_extra or {} - field_info = FieldInfo( - default, - default_factory=default_factory, - alias=alias, - title=title, - description=description, - exclude=exclude, - include=include, - const=const, - gt=gt, - ge=ge, - lt=lt, - le=le, - multiple_of=multiple_of, - max_digits=max_digits, - decimal_places=decimal_places, - min_items=min_items, - max_items=max_items, - unique_items=unique_items, - min_length=min_length, - max_length=max_length, - allow_mutation=allow_mutation, - regex=regex, - discriminator=discriminator, - repr=repr, - primary_key=primary_key, - foreign_key=foreign_key, - ondelete=ondelete, - unique=unique, - nullable=nullable, - index=index, - sa_type=sa_type, - sa_column=sa_column, - sa_column_args=sa_column_args, - sa_column_kwargs=sa_column_kwargs, + field_info_kwargs = { + "alias": alias, + "validation_alias": validation_alias, + "serialization_alias": serialization_alias, + "title": title, + "description": description, + "exclude": exclude, + "include": include, + "const": const, + "gt": gt, + "ge": ge, + "lt": lt, + "le": le, + "multiple_of": multiple_of, + "max_digits": max_digits, + "decimal_places": decimal_places, + "min_items": min_items, + "max_items": max_items, + "unique_items": unique_items, + "min_length": min_length, + "max_length": max_length, + "allow_mutation": allow_mutation, + "regex": regex, + "discriminator": discriminator, + "repr": repr, + "primary_key": primary_key, + "foreign_key": foreign_key, + "ondelete": ondelete, + "unique": unique, + "nullable": nullable, + "index": index, + "sa_type": sa_type, + "sa_column": sa_column, + "sa_column_args": sa_column_args, + "sa_column_kwargs": sa_column_kwargs, **current_schema_extra, - ) + } + if IS_PYDANTIC_V2: + field_info = FieldInfo( + default, + default_factory=default_factory, + **field_info_kwargs, + ) + else: + if validation_alias: + raise RuntimeError("validation_alias is not supported in Pydantic v1") + if serialization_alias: + raise RuntimeError("serialization_alias is not supported in Pydantic v1") + field_info = FieldInfo( + default, + default_factory=default_factory, + **field_info_kwargs, + ) + post_init_field_info(field_info) return field_info diff --git a/tests/test_aliases.py b/tests/test_aliases.py new file mode 100644 index 0000000000..ffb77045b1 --- /dev/null +++ b/tests/test_aliases.py @@ -0,0 +1,161 @@ +from typing import Type, Union + +import pytest +from pydantic import VERSION, BaseModel, ValidationError +from pydantic import Field as PField +from sqlmodel import Field, SQLModel + + +# ----------------------------------------------------------------------------------- +# Models + + +class PydanticUser(BaseModel): + full_name: str = PField(alias="fullName") + + +class SQLModelUser(SQLModel): + full_name: str = Field(alias="fullName") + + +# Models with config (validate_by_name=True) + + +if VERSION.startswith("2."): + + class PydanticUserWithConfig(PydanticUser): + model_config = {"validate_by_name": True} + + class SQLModelUserWithConfig(SQLModelUser): + model_config = {"validate_by_name": True} + +else: + + class PydanticUserWithConfig(PydanticUser): + class Config: + allow_population_by_field_name = True + + class SQLModelUserWithConfig(SQLModelUser): + class Config: + allow_population_by_field_name = True + + +# ----------------------------------------------------------------------------------- +# Tests + +# Test validate by name + + +@pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) +def test_create_with_field_name(model: Union[Type[PydanticUser], Type[SQLModelUser]]): + with pytest.raises(ValidationError): + model(full_name="Alice") + + +@pytest.mark.parametrize("model", [PydanticUserWithConfig, SQLModelUserWithConfig]) +def test_create_with_field_name_with_config( + model: Union[Type[PydanticUserWithConfig], Type[SQLModelUserWithConfig]], +): + user = model(full_name="Alice") + assert user.full_name == "Alice" + + +# Test validate by alias + + +@pytest.mark.parametrize( + "model", + [PydanticUser, SQLModelUser, PydanticUserWithConfig, SQLModelUserWithConfig], +) +def test_create_with_alias( + model: Union[ + Type[PydanticUser], + Type[SQLModelUser], + Type[PydanticUserWithConfig], + Type[SQLModelUserWithConfig], + ], +): + user = model(fullName="Bob") # using alias + assert user.full_name == "Bob" + + +# Test validate by name and alias + + +@pytest.mark.parametrize("model", [PydanticUserWithConfig, SQLModelUserWithConfig]) +def test_create_with_both_prefers_alias( + model: Union[Type[PydanticUserWithConfig], Type[SQLModelUserWithConfig]], +): + user = model(full_name="IGNORED", fullName="Charlie") + assert user.full_name == "Charlie" # alias should take precedence + + +# Test serialize + + +@pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) +def test_dict_default_uses_field_names( + model: Union[Type[PydanticUser], Type[SQLModelUser]], +): + user = model(fullName="Dana") + data = user.dict() + assert "full_name" in data + assert "fullName" not in data + assert data["full_name"] == "Dana" + + +# Test serialize by alias + + +@pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) +def test_dict_default_uses_aliases( + model: Union[Type[PydanticUser], Type[SQLModelUser]], +): + user = model(fullName="Dana") + data = user.dict(by_alias=True) + assert "fullName" in data + assert "full_name" not in data + assert data["fullName"] == "Dana" + + +# Test json by alias + + +@pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) +def test_json_by_alias( + model: Union[Type[PydanticUser], Type[SQLModelUser]], +): + user = model(fullName="Frank") + json_data = user.json(by_alias=True) + assert ('"fullName":"Frank"' in json_data) or ('"fullName": "Frank"' in json_data) + assert "full_name" not in json_data + + +class PydanticUserV2(BaseModel): + first_name: str = PField( + validation_alias="firstName", serialization_alias="f_name" + ) + + +class SQLModelUserV2(SQLModel): + first_name: str = Field( + validation_alias="firstName", serialization_alias="f_name" + ) + + +@pytest.mark.parametrize("model", [PydanticUserV2, SQLModelUserV2]) +def test_create_with_validation_alias(model: Union[Type[PydanticUserV2], Type[SQLModelUserV2]]): + user = model(firstName="John") + assert user.first_name == "John" + + +@pytest.mark.parametrize("model", [PydanticUserV2, SQLModelUserV2]) +def test_serialize_with_serialization_alias( + model: Union[Type[PydanticUserV2], Type[SQLModelUserV2]] +): + user = model(firstName="Jane") + data = user.dict(by_alias=True) + assert "f_name" in data + assert "firstName" not in data + assert "first_name" not in data + assert data["f_name"] == "Jane" \ No newline at end of file From 72e8c36b914c2aefbb8208a7d3933328663e7506 Mon Sep 17 00:00:00 2001 From: Ravishankar Sivasubramaniam Date: Thu, 25 Sep 2025 16:50:13 -0500 Subject: [PATCH 02/15] fix: Skip alias tests in Pydantic v1 --- tests/test_aliases.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_aliases.py b/tests/test_aliases.py index ffb77045b1..cb59b307db 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -143,12 +143,14 @@ class SQLModelUserV2(SQLModel): ) +@pytest.mark.skipif(not VERSION.startswith("2."), reason="validation_alias and serialization_alias are not supported in Pydantic v1") @pytest.mark.parametrize("model", [PydanticUserV2, SQLModelUserV2]) def test_create_with_validation_alias(model: Union[Type[PydanticUserV2], Type[SQLModelUserV2]]): user = model(firstName="John") assert user.first_name == "John" +@pytest.mark.skipif(not VERSION.startswith("2."), reason="validation_alias and serialization_alias are not supported in Pydantic v1") @pytest.mark.parametrize("model", [PydanticUserV2, SQLModelUserV2]) def test_serialize_with_serialization_alias( model: Union[Type[PydanticUserV2], Type[SQLModelUserV2]] From 15c79549cf56a72c186ca5683ead32a8b94226a1 Mon Sep 17 00:00:00 2001 From: Ravishankar Sivasubramaniam Date: Thu, 25 Sep 2025 17:17:28 -0500 Subject: [PATCH 03/15] feat: Add support for Pydantic v2 aliases --- sqlmodel/_compat.py | 1 + tests/test_aliases.py | 27 +++++++++++++++------------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 0ed19b925b..f8bbb46e7b 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -228,6 +228,7 @@ def post_init_field_info(field_info: FieldInfo) -> None: field_info.serialization_alias = field_info.alias else: field_info._validate() # type: ignore[attr-defined] + # Dummy to make it importable def _calculate_keys( self: "SQLModel", diff --git a/tests/test_aliases.py b/tests/test_aliases.py index cb59b307db..c0d2534fdc 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -5,7 +5,6 @@ from pydantic import Field as PField from sqlmodel import Field, SQLModel - # ----------------------------------------------------------------------------------- # Models @@ -132,32 +131,36 @@ def test_json_by_alias( class PydanticUserV2(BaseModel): - first_name: str = PField( - validation_alias="firstName", serialization_alias="f_name" - ) + first_name: str = PField(validation_alias="firstName", serialization_alias="f_name") class SQLModelUserV2(SQLModel): - first_name: str = Field( - validation_alias="firstName", serialization_alias="f_name" - ) + first_name: str = Field(validation_alias="firstName", serialization_alias="f_name") -@pytest.mark.skipif(not VERSION.startswith("2."), reason="validation_alias and serialization_alias are not supported in Pydantic v1") +@pytest.mark.skipif( + not VERSION.startswith("2."), + reason="validation_alias and serialization_alias are not supported in Pydantic v1", +) @pytest.mark.parametrize("model", [PydanticUserV2, SQLModelUserV2]) -def test_create_with_validation_alias(model: Union[Type[PydanticUserV2], Type[SQLModelUserV2]]): +def test_create_with_validation_alias( + model: Union[Type[PydanticUserV2], Type[SQLModelUserV2]], +): user = model(firstName="John") assert user.first_name == "John" -@pytest.mark.skipif(not VERSION.startswith("2."), reason="validation_alias and serialization_alias are not supported in Pydantic v1") +@pytest.mark.skipif( + not VERSION.startswith("2."), + reason="validation_alias and serialization_alias are not supported in Pydantic v1", +) @pytest.mark.parametrize("model", [PydanticUserV2, SQLModelUserV2]) def test_serialize_with_serialization_alias( - model: Union[Type[PydanticUserV2], Type[SQLModelUserV2]] + model: Union[Type[PydanticUserV2], Type[SQLModelUserV2]], ): user = model(firstName="Jane") data = user.dict(by_alias=True) assert "f_name" in data assert "firstName" not in data assert "first_name" not in data - assert data["f_name"] == "Jane" \ No newline at end of file + assert data["f_name"] == "Jane" From 3cc2b1e3e616b9ca8648d4dcc91b1f83bc566496 Mon Sep 17 00:00:00 2001 From: Ravishankar Sivasubramaniam Date: Thu, 25 Sep 2025 17:27:52 -0500 Subject: [PATCH 04/15] fix: Skip alias tests in Pydantic v1 --- tests/test_aliases.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/test_aliases.py b/tests/test_aliases.py index c0d2534fdc..743d74db7f 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -97,7 +97,7 @@ def test_dict_default_uses_field_names( model: Union[Type[PydanticUser], Type[SQLModelUser]], ): user = model(fullName="Dana") - data = user.dict() + data = user.model_dump() assert "full_name" in data assert "fullName" not in data assert data["full_name"] == "Dana" @@ -111,7 +111,7 @@ def test_dict_default_uses_aliases( model: Union[Type[PydanticUser], Type[SQLModelUser]], ): user = model(fullName="Dana") - data = user.dict(by_alias=True) + data = user.model_dump(by_alias=True) assert "fullName" in data assert "full_name" not in data assert data["fullName"] == "Dana" @@ -125,17 +125,27 @@ def test_json_by_alias( model: Union[Type[PydanticUser], Type[SQLModelUser]], ): user = model(fullName="Frank") - json_data = user.json(by_alias=True) + json_data = user.model_dump_json(by_alias=True) assert ('"fullName":"Frank"' in json_data) or ('"fullName": "Frank"' in json_data) assert "full_name" not in json_data -class PydanticUserV2(BaseModel): - first_name: str = PField(validation_alias="firstName", serialization_alias="f_name") +# Pydantic v2 specific models - only define if we're running Pydantic v2 +if VERSION.startswith("2."): + class PydanticUserV2(BaseModel): + first_name: str = PField( + validation_alias="firstName", serialization_alias="f_name" + ) -class SQLModelUserV2(SQLModel): - first_name: str = Field(validation_alias="firstName", serialization_alias="f_name") + class SQLModelUserV2(SQLModel): + first_name: str = Field( + validation_alias="firstName", serialization_alias="f_name" + ) +else: + # Dummy classes for Pydantic v1 to prevent import errors + PydanticUserV2 = None + SQLModelUserV2 = None @pytest.mark.skipif( @@ -159,7 +169,7 @@ def test_serialize_with_serialization_alias( model: Union[Type[PydanticUserV2], Type[SQLModelUserV2]], ): user = model(firstName="Jane") - data = user.dict(by_alias=True) + data = user.model_dump(by_alias=True) assert "f_name" in data assert "firstName" not in data assert "first_name" not in data From 34d2a4500e26a62f7b9e30982eebd44538e08859 Mon Sep 17 00:00:00 2001 From: Ravishankar Sivasubramaniam Date: Thu, 25 Sep 2025 17:39:06 -0500 Subject: [PATCH 05/15] fix: Add Pydantic v2 alias support with backward compatibility This commit fixes Field(alias="...") functionality in Pydantic v2 while maintaining full backward compatibility with Pydantic v1. --- sqlmodel/main.py | 9 +++++++-- tests/test_aliases.py | 8 ++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 1578b030bf..8866ac7282 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -397,8 +397,6 @@ def Field( current_schema_extra = schema_extra or {} field_info_kwargs = { "alias": alias, - "validation_alias": validation_alias, - "serialization_alias": serialization_alias, "title": title, "description": description, "exclude": exclude, @@ -433,6 +431,13 @@ def Field( **current_schema_extra, } if IS_PYDANTIC_V2: + # Add Pydantic v2 specific parameters + field_info_kwargs.update( + { + "validation_alias": validation_alias, + "serialization_alias": serialization_alias, + } + ) field_info = FieldInfo( default, default_factory=default_factory, diff --git a/tests/test_aliases.py b/tests/test_aliases.py index 743d74db7f..f384abf010 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -97,7 +97,7 @@ def test_dict_default_uses_field_names( model: Union[Type[PydanticUser], Type[SQLModelUser]], ): user = model(fullName="Dana") - data = user.model_dump() + data = user.dict() assert "full_name" in data assert "fullName" not in data assert data["full_name"] == "Dana" @@ -111,7 +111,7 @@ def test_dict_default_uses_aliases( model: Union[Type[PydanticUser], Type[SQLModelUser]], ): user = model(fullName="Dana") - data = user.model_dump(by_alias=True) + data = user.dict(by_alias=True) assert "fullName" in data assert "full_name" not in data assert data["fullName"] == "Dana" @@ -125,7 +125,7 @@ def test_json_by_alias( model: Union[Type[PydanticUser], Type[SQLModelUser]], ): user = model(fullName="Frank") - json_data = user.model_dump_json(by_alias=True) + json_data = user.json(by_alias=True) assert ('"fullName":"Frank"' in json_data) or ('"fullName": "Frank"' in json_data) assert "full_name" not in json_data @@ -169,7 +169,7 @@ def test_serialize_with_serialization_alias( model: Union[Type[PydanticUserV2], Type[SQLModelUserV2]], ): user = model(firstName="Jane") - data = user.model_dump(by_alias=True) + data = user.dict(by_alias=True) assert "f_name" in data assert "firstName" not in data assert "first_name" not in data From 66671099e762db0972814030d246d6d13695c7f2 Mon Sep 17 00:00:00 2001 From: Ravishankar Sivasubramaniam Date: Tue, 30 Sep 2025 16:49:54 -0500 Subject: [PATCH 06/15] Apply suggestion from @YuriiMotov Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- tests/test_aliases.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_aliases.py b/tests/test_aliases.py index f384abf010..eca244b122 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -148,10 +148,7 @@ class SQLModelUserV2(SQLModel): SQLModelUserV2 = None -@pytest.mark.skipif( - not VERSION.startswith("2."), - reason="validation_alias and serialization_alias are not supported in Pydantic v1", -) +@needs_pydanticv2 @pytest.mark.parametrize("model", [PydanticUserV2, SQLModelUserV2]) def test_create_with_validation_alias( model: Union[Type[PydanticUserV2], Type[SQLModelUserV2]], From 382a52da35ac74a7c57726cb72ae4f37bbd2c1f8 Mon Sep 17 00:00:00 2001 From: Ravishankar Sivasubramaniam Date: Tue, 30 Sep 2025 17:03:54 -0500 Subject: [PATCH 07/15] fix: move this outside of if..else.. block to avoid duplicati as per Yurii's comment https://github.com/fastapi/sqlmodel/pull/1577/files/34d2a4500e26a62f7b9e30982eebd44538e08859\#r2392422553 --- sqlmodel/main.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 8866ac7282..e9b732a369 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -438,21 +438,16 @@ def Field( "serialization_alias": serialization_alias, } ) - field_info = FieldInfo( - default, - default_factory=default_factory, - **field_info_kwargs, - ) else: if validation_alias: raise RuntimeError("validation_alias is not supported in Pydantic v1") if serialization_alias: raise RuntimeError("serialization_alias is not supported in Pydantic v1") - field_info = FieldInfo( - default, - default_factory=default_factory, - **field_info_kwargs, - ) + field_info = FieldInfo( + default, + default_factory=default_factory, + **field_info_kwargs, + ) post_init_field_info(field_info) return field_info From d3e761ebc5c965d39c60b7510264c83e29d6d422 Mon Sep 17 00:00:00 2001 From: Ravishankar Sivasubramaniam Date: Tue, 30 Sep 2025 18:28:13 -0500 Subject: [PATCH 08/15] fix: Addressed yurii's comments --- tests/test_aliases.py | 69 +++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/tests/test_aliases.py b/tests/test_aliases.py index eca244b122..ea32002a95 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -5,8 +5,11 @@ from pydantic import Field as PField from sqlmodel import Field, SQLModel -# ----------------------------------------------------------------------------------- -# Models +from tests.conftest import needs_pydanticv2 + +""" +Alias tests for SQLModel and Pydantic compatibility +""" class PydanticUser(BaseModel): @@ -39,12 +42,6 @@ class Config: allow_population_by_field_name = True -# ----------------------------------------------------------------------------------- -# Tests - -# Test validate by name - - @pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) def test_create_with_field_name(model: Union[Type[PydanticUser], Type[SQLModelUser]]): with pytest.raises(ValidationError): @@ -59,9 +56,6 @@ def test_create_with_field_name_with_config( assert user.full_name == "Alice" -# Test validate by alias - - @pytest.mark.parametrize( "model", [PydanticUser, SQLModelUser, PydanticUserWithConfig, SQLModelUserWithConfig], @@ -78,9 +72,6 @@ def test_create_with_alias( assert user.full_name == "Bob" -# Test validate by name and alias - - @pytest.mark.parametrize("model", [PydanticUserWithConfig, SQLModelUserWithConfig]) def test_create_with_both_prefers_alias( model: Union[Type[PydanticUserWithConfig], Type[SQLModelUserWithConfig]], @@ -89,48 +80,47 @@ def test_create_with_both_prefers_alias( assert user.full_name == "Charlie" # alias should take precedence -# Test serialize - - @pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) def test_dict_default_uses_field_names( model: Union[Type[PydanticUser], Type[SQLModelUser]], ): user = model(fullName="Dana") - data = user.dict() + if VERSION.startswith("2."): + data = user.model_dump() + else: + data = user.dict() assert "full_name" in data assert "fullName" not in data assert data["full_name"] == "Dana" -# Test serialize by alias - - @pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) def test_dict_default_uses_aliases( model: Union[Type[PydanticUser], Type[SQLModelUser]], ): user = model(fullName="Dana") - data = user.dict(by_alias=True) + if VERSION.startswith("2."): + data = user.model_dump(by_alias=True) + else: + data = user.dict(by_alias=True) assert "fullName" in data assert "full_name" not in data assert data["fullName"] == "Dana" -# Test json by alias - - @pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) def test_json_by_alias( model: Union[Type[PydanticUser], Type[SQLModelUser]], ): user = model(fullName="Frank") - json_data = user.json(by_alias=True) + if VERSION.startswith("2."): + json_data = user.model_dump_json(by_alias=True) + else: + json_data = user.json(by_alias=True) assert ('"fullName":"Frank"' in json_data) or ('"fullName": "Frank"' in json_data) assert "full_name" not in json_data -# Pydantic v2 specific models - only define if we're running Pydantic v2 if VERSION.startswith("2."): class PydanticUserV2(BaseModel): @@ -148,6 +138,24 @@ class SQLModelUserV2(SQLModel): SQLModelUserV2 = None +def test_validation_alias_runtimeerror_pydantic_v1(): + if VERSION.startswith("2."): + pytest.skip("Only relevant for Pydantic v1") + with pytest.raises( + RuntimeError, match="validation_alias is not supported in Pydantic v1" + ): + Field(validation_alias="foo") + + +def test_serialization_alias_runtimeerror_pydantic_v1(): + if VERSION.startswith("2."): + pytest.skip("Only relevant for Pydantic v1") + with pytest.raises( + RuntimeError, match="serialization_alias is not supported in Pydantic v1" + ): + Field(serialization_alias="bar") + + @needs_pydanticv2 @pytest.mark.parametrize("model", [PydanticUserV2, SQLModelUserV2]) def test_create_with_validation_alias( @@ -157,16 +165,13 @@ def test_create_with_validation_alias( assert user.first_name == "John" -@pytest.mark.skipif( - not VERSION.startswith("2."), - reason="validation_alias and serialization_alias are not supported in Pydantic v1", -) +@needs_pydanticv2 @pytest.mark.parametrize("model", [PydanticUserV2, SQLModelUserV2]) def test_serialize_with_serialization_alias( model: Union[Type[PydanticUserV2], Type[SQLModelUserV2]], ): user = model(firstName="Jane") - data = user.dict(by_alias=True) + data = user.model_dump(by_alias=True) assert "f_name" in data assert "firstName" not in data assert "first_name" not in data From e07a2590e6b39cbb6a0b1746802c91e9153502d4 Mon Sep 17 00:00:00 2001 From: Ravishankar Sivasubramaniam Date: Wed, 8 Oct 2025 17:38:19 -0500 Subject: [PATCH 09/15] Update tests/test_aliases.py Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- tests/test_aliases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_aliases.py b/tests/test_aliases.py index ea32002a95..8d49d63f37 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -5,7 +5,7 @@ from pydantic import Field as PField from sqlmodel import Field, SQLModel -from tests.conftest import needs_pydanticv2 +from tests.conftest import needs_pydanticv1, needs_pydanticv2 """ Alias tests for SQLModel and Pydantic compatibility From 6b7a0a10e4b9042668d2618673f75aaa49537eea Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 22:38:25 +0000 Subject: [PATCH 10/15] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_aliases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_aliases.py b/tests/test_aliases.py index 8d49d63f37..ea32002a95 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -5,7 +5,7 @@ from pydantic import Field as PField from sqlmodel import Field, SQLModel -from tests.conftest import needs_pydanticv1, needs_pydanticv2 +from tests.conftest import needs_pydanticv2 """ Alias tests for SQLModel and Pydantic compatibility From 322c6affe508855c767fcc58c2129fcd4ebc9e77 Mon Sep 17 00:00:00 2001 From: Ravishankar Sivasubramaniam Date: Wed, 8 Oct 2025 19:14:36 -0500 Subject: [PATCH 11/15] Field: support schema_extra aliases (v2); add alias tests --- sqlmodel/main.py | 27 +++++++++---- tests/test_aliases.py | 92 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 104 insertions(+), 15 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index cddb1fce37..ca826c0cfe 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -398,6 +398,9 @@ def Field( schema_extra: Optional[Dict[str, Any]] = None, ) -> Any: current_schema_extra = schema_extra or {} + # Extract possible alias settings from schema_extra so we can control precedence + schema_validation_alias = current_schema_extra.pop("validation_alias", None) + schema_serialization_alias = current_schema_extra.pop("serialization_alias", None) field_info_kwargs = { "alias": alias, "title": title, @@ -434,17 +437,25 @@ def Field( **current_schema_extra, } if IS_PYDANTIC_V2: - # Add Pydantic v2 specific parameters - field_info_kwargs.update( - { - "validation_alias": validation_alias, - "serialization_alias": serialization_alias, - } + # explicit params > schema_extra > alias propagation (handled later) + effective_validation_alias = ( + validation_alias + if validation_alias is not None + else schema_validation_alias ) + effective_serialization_alias = ( + serialization_alias + if serialization_alias is not None + else schema_serialization_alias + ) + if effective_validation_alias is not None: + field_info_kwargs["validation_alias"] = effective_validation_alias + if effective_serialization_alias is not None: + field_info_kwargs["serialization_alias"] = effective_serialization_alias else: - if validation_alias: + if validation_alias or schema_validation_alias is not None: raise RuntimeError("validation_alias is not supported in Pydantic v1") - if serialization_alias: + if serialization_alias or schema_serialization_alias is not None: raise RuntimeError("serialization_alias is not supported in Pydantic v1") field_info = FieldInfo( default, diff --git a/tests/test_aliases.py b/tests/test_aliases.py index ea32002a95..ef1c65bb81 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -5,7 +5,7 @@ from pydantic import Field as PField from sqlmodel import Field, SQLModel -from tests.conftest import needs_pydanticv2 +from tests.conftest import needs_pydanticv1, needs_pydanticv2 """ Alias tests for SQLModel and Pydantic compatibility @@ -21,8 +21,6 @@ class SQLModelUser(SQLModel): # Models with config (validate_by_name=True) - - if VERSION.startswith("2."): class PydanticUserWithConfig(PydanticUser): @@ -138,18 +136,16 @@ class SQLModelUserV2(SQLModel): SQLModelUserV2 = None +@needs_pydanticv1 def test_validation_alias_runtimeerror_pydantic_v1(): - if VERSION.startswith("2."): - pytest.skip("Only relevant for Pydantic v1") with pytest.raises( RuntimeError, match="validation_alias is not supported in Pydantic v1" ): Field(validation_alias="foo") +@needs_pydanticv1 def test_serialization_alias_runtimeerror_pydantic_v1(): - if VERSION.startswith("2."): - pytest.skip("Only relevant for Pydantic v1") with pytest.raises( RuntimeError, match="serialization_alias is not supported in Pydantic v1" ): @@ -176,3 +172,85 @@ def test_serialize_with_serialization_alias( assert "firstName" not in data assert "first_name" not in data assert data["f_name"] == "Jane" + + +@needs_pydanticv2 +def test_schema_extra_validation_alias_sqlmodel_v2(): + class M(SQLModel): + f: str = Field(schema_extra={"validation_alias": "f_alias"}) + + m = M.model_validate({"f_alias": "asd"}) + assert m.f == "asd" + + +@needs_pydanticv2 +def test_schema_extra_serialization_alias_sqlmodel_v2(): + class M(SQLModel): + f: str = Field(schema_extra={"serialization_alias": "f_out"}) + + m = M(f="x") + data = m.model_dump(by_alias=True) + assert "f_out" in data + assert "f" not in data + assert data["f_out"] == "x" + + +@needs_pydanticv1 +def test_schema_extra_validation_alias_runtimeerror_pydantic_v1(): + with pytest.raises( + RuntimeError, match="validation_alias is not supported in Pydantic v1" + ): + Field(schema_extra={"validation_alias": "x"}) + + +@needs_pydanticv1 +def test_schema_extra_serialization_alias_runtimeerror_pydantic_v1(): + with pytest.raises( + RuntimeError, match="serialization_alias is not supported in Pydantic v1" + ): + Field(schema_extra={"serialization_alias": "y"}) + + +@needs_pydanticv2 +def test_alias_plus_validation_alias_prefers_validation_alias_sqlmodel_v2(): + class M(SQLModel): + first_name: str = Field(alias="fullName", validation_alias="v_name") + + m = M.model_validate({"fullName": "A", "v_name": "B"}) + assert m.first_name == "B" + + +@needs_pydanticv2 +def test_alias_plus_serialization_alias_prefers_serialization_alias_sqlmodel_v2(): + class M(SQLModel): + first_name: str = Field(alias="fullName", serialization_alias="f_name") + + m = M(first_name="Z") + data = m.model_dump(by_alias=True) + assert "f_name" in data + assert "fullName" not in data + assert data["f_name"] == "Z" + + +@needs_pydanticv2 +def test_alias_generator_works_sqlmodel_v2(): + class M(SQLModel): + model_config = {"alias_generator": lambda s: "gen_" + s} + f: str = Field() + + m = M.model_validate({"gen_f": "ok"}) + assert m.f == "ok" + data = m.model_dump(by_alias=True) + assert "gen_f" in data and data["gen_f"] == "ok" + + +@needs_pydanticv2 +def test_alias_generator_with_explicit_alias_prefers_field_alias_sqlmodel_v2(): + class M(SQLModel): + model_config = {"alias_generator": lambda s: "gen_" + s} + f: str = Field(alias="custom") + + m = M.model_validate({"custom": "ok"}) + assert m.f == "ok" + data = m.model_dump(by_alias=True) + assert "custom" in data and "gen_f" not in data From 6b133811da925d2e8c417b5d40831558ba1e1779 Mon Sep 17 00:00:00 2001 From: Ravishankar Sivasubramaniam Date: Wed, 8 Oct 2025 19:15:01 -0500 Subject: [PATCH 12/15] Field: support schema_extra aliases (v2); add alias tests --- tests/test_aliases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_aliases.py b/tests/test_aliases.py index ef1c65bb81..c2ee03842e 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -225,7 +225,7 @@ def test_alias_plus_serialization_alias_prefers_serialization_alias_sqlmodel_v2( class M(SQLModel): first_name: str = Field(alias="fullName", serialization_alias="f_name") - m = M(first_name="Z") + m = M(fullName="Z") data = m.model_dump(by_alias=True) assert "f_name" in data assert "fullName" not in data From 7b2bc2c212792ad106405912ffc4b6941693fbff Mon Sep 17 00:00:00 2001 From: Ravishankar Sivasubramaniam Date: Wed, 8 Oct 2025 20:43:53 -0500 Subject: [PATCH 13/15] compat: drop redundant v2 check in post_init_field_info; --- sqlmodel/_compat.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 32eb1980df..28b372d103 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -235,13 +235,10 @@ def get_field_metadata(field: Any) -> Any: return FakeMetadata() def post_init_field_info(field_info: FieldInfo) -> None: - if IS_PYDANTIC_V2: - if field_info.alias and not field_info.validation_alias: - field_info.validation_alias = field_info.alias - if field_info.alias and not field_info.serialization_alias: - field_info.serialization_alias = field_info.alias - else: - field_info._validate() # type: ignore[attr-defined] + if field_info.alias and not field_info.validation_alias: + field_info.validation_alias = field_info.alias + if field_info.alias and not field_info.serialization_alias: + field_info.serialization_alias = field_info.alias # Dummy to make it importable def _calculate_keys( From 68b31d71dece6b33c88be0260008b9a3ef1d6816 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Thu, 9 Oct 2025 18:39:11 +0200 Subject: [PATCH 14/15] Simplify alias propagation logic for Pydantic v2 --- sqlmodel/_compat.py | 5 +---- sqlmodel/main.py | 18 +++++------------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/sqlmodel/_compat.py b/sqlmodel/_compat.py index 28b372d103..230f8cc362 100644 --- a/sqlmodel/_compat.py +++ b/sqlmodel/_compat.py @@ -235,10 +235,7 @@ def get_field_metadata(field: Any) -> Any: return FakeMetadata() def post_init_field_info(field_info: FieldInfo) -> None: - if field_info.alias and not field_info.validation_alias: - field_info.validation_alias = field_info.alias - if field_info.alias and not field_info.serialization_alias: - field_info.serialization_alias = field_info.alias + return None # Dummy to make it importable def _calculate_keys( diff --git a/sqlmodel/main.py b/sqlmodel/main.py index ca826c0cfe..087f80614f 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -437,21 +437,13 @@ def Field( **current_schema_extra, } if IS_PYDANTIC_V2: - # explicit params > schema_extra > alias propagation (handled later) - effective_validation_alias = ( - validation_alias - if validation_alias is not None - else schema_validation_alias + # explicit params > schema_extra > alias propagation + field_info_kwargs["validation_alias"] = ( + validation_alias or schema_validation_alias or alias ) - effective_serialization_alias = ( - serialization_alias - if serialization_alias is not None - else schema_serialization_alias + field_info_kwargs["serialization_alias"] = ( + serialization_alias or schema_serialization_alias or alias ) - if effective_validation_alias is not None: - field_info_kwargs["validation_alias"] = effective_validation_alias - if effective_serialization_alias is not None: - field_info_kwargs["serialization_alias"] = effective_serialization_alias else: if validation_alias or schema_validation_alias is not None: raise RuntimeError("validation_alias is not supported in Pydantic v1") From 9440956d9221b2c3a266498594471b3d3b549ade Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Thu, 9 Oct 2025 18:39:37 +0200 Subject: [PATCH 15/15] Fix test name and avoid deprecation warnings --- tests/test_aliases.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_aliases.py b/tests/test_aliases.py index c2ee03842e..99c89f4a2d 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -83,7 +83,7 @@ def test_dict_default_uses_field_names( model: Union[Type[PydanticUser], Type[SQLModelUser]], ): user = model(fullName="Dana") - if VERSION.startswith("2."): + if VERSION.startswith("2.") or isinstance(user, SQLModel): data = user.model_dump() else: data = user.dict() @@ -93,11 +93,11 @@ def test_dict_default_uses_field_names( @pytest.mark.parametrize("model", [PydanticUser, SQLModelUser]) -def test_dict_default_uses_aliases( +def test_dict_by_alias_uses_aliases( model: Union[Type[PydanticUser], Type[SQLModelUser]], ): user = model(fullName="Dana") - if VERSION.startswith("2."): + if VERSION.startswith("2.") or isinstance(user, SQLModel): data = user.model_dump(by_alias=True) else: data = user.dict(by_alias=True)