Skip to content

Commit 785270a

Browse files
committed
feat: Add support for Pydantic v2 aliases
1 parent c6acc1f commit 785270a

3 files changed

Lines changed: 229 additions & 38 deletions

File tree

sqlmodel/_compat.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,13 @@ def get_field_metadata(field: Any) -> Any:
221221
return FakeMetadata()
222222

223223
def post_init_field_info(field_info: FieldInfo) -> None:
224-
return None
225-
224+
if IS_PYDANTIC_V2:
225+
if field_info.alias and not field_info.validation_alias:
226+
field_info.validation_alias = field_info.alias
227+
if field_info.alias and not field_info.serialization_alias:
228+
field_info.serialization_alias = field_info.alias
229+
else:
230+
field_info._validate() # type: ignore[attr-defined]
226231
# Dummy to make it importable
227232
def _calculate_keys(
228233
self: "SQLModel",

sqlmodel/main.py

Lines changed: 61 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ def Field(
215215
*,
216216
default_factory: Optional[NoArgAnyCallable] = None,
217217
alias: Optional[str] = None,
218+
validation_alias: Optional[str] = None,
219+
serialization_alias: Optional[str] = None,
218220
title: Optional[str] = None,
219221
description: Optional[str] = None,
220222
exclude: Union[
@@ -260,6 +262,8 @@ def Field(
260262
*,
261263
default_factory: Optional[NoArgAnyCallable] = None,
262264
alias: Optional[str] = None,
265+
validation_alias: Optional[str] = None,
266+
serialization_alias: Optional[str] = None,
263267
title: Optional[str] = None,
264268
description: Optional[str] = None,
265269
exclude: Union[
@@ -314,6 +318,8 @@ def Field(
314318
*,
315319
default_factory: Optional[NoArgAnyCallable] = None,
316320
alias: Optional[str] = None,
321+
validation_alias: Optional[str] = None,
322+
serialization_alias: Optional[str] = None,
317323
title: Optional[str] = None,
318324
description: Optional[str] = None,
319325
exclude: Union[
@@ -349,6 +355,8 @@ def Field(
349355
*,
350356
default_factory: Optional[NoArgAnyCallable] = None,
351357
alias: Optional[str] = None,
358+
validation_alias: Optional[str] = None,
359+
serialization_alias: Optional[str] = None,
352360
title: Optional[str] = None,
353361
description: Optional[str] = None,
354362
exclude: Union[
@@ -387,43 +395,60 @@ def Field(
387395
schema_extra: Optional[Dict[str, Any]] = None,
388396
) -> Any:
389397
current_schema_extra = schema_extra or {}
390-
field_info = FieldInfo(
391-
default,
392-
default_factory=default_factory,
393-
alias=alias,
394-
title=title,
395-
description=description,
396-
exclude=exclude,
397-
include=include,
398-
const=const,
399-
gt=gt,
400-
ge=ge,
401-
lt=lt,
402-
le=le,
403-
multiple_of=multiple_of,
404-
max_digits=max_digits,
405-
decimal_places=decimal_places,
406-
min_items=min_items,
407-
max_items=max_items,
408-
unique_items=unique_items,
409-
min_length=min_length,
410-
max_length=max_length,
411-
allow_mutation=allow_mutation,
412-
regex=regex,
413-
discriminator=discriminator,
414-
repr=repr,
415-
primary_key=primary_key,
416-
foreign_key=foreign_key,
417-
ondelete=ondelete,
418-
unique=unique,
419-
nullable=nullable,
420-
index=index,
421-
sa_type=sa_type,
422-
sa_column=sa_column,
423-
sa_column_args=sa_column_args,
424-
sa_column_kwargs=sa_column_kwargs,
398+
field_info_kwargs = {
399+
"alias": alias,
400+
"validation_alias": validation_alias,
401+
"serialization_alias": serialization_alias,
402+
"title": title,
403+
"description": description,
404+
"exclude": exclude,
405+
"include": include,
406+
"const": const,
407+
"gt": gt,
408+
"ge": ge,
409+
"lt": lt,
410+
"le": le,
411+
"multiple_of": multiple_of,
412+
"max_digits": max_digits,
413+
"decimal_places": decimal_places,
414+
"min_items": min_items,
415+
"max_items": max_items,
416+
"unique_items": unique_items,
417+
"min_length": min_length,
418+
"max_length": max_length,
419+
"allow_mutation": allow_mutation,
420+
"regex": regex,
421+
"discriminator": discriminator,
422+
"repr": repr,
423+
"primary_key": primary_key,
424+
"foreign_key": foreign_key,
425+
"ondelete": ondelete,
426+
"unique": unique,
427+
"nullable": nullable,
428+
"index": index,
429+
"sa_type": sa_type,
430+
"sa_column": sa_column,
431+
"sa_column_args": sa_column_args,
432+
"sa_column_kwargs": sa_column_kwargs,
425433
**current_schema_extra,
426-
)
434+
}
435+
if IS_PYDANTIC_V2:
436+
field_info = FieldInfo(
437+
default,
438+
default_factory=default_factory,
439+
**field_info_kwargs,
440+
)
441+
else:
442+
if validation_alias:
443+
raise RuntimeError("validation_alias is not supported in Pydantic v1")
444+
if serialization_alias:
445+
raise RuntimeError("serialization_alias is not supported in Pydantic v1")
446+
field_info = FieldInfo(
447+
default,
448+
default_factory=default_factory,
449+
**field_info_kwargs,
450+
)
451+
427452
post_init_field_info(field_info)
428453
return field_info
429454

tests/test_aliases.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
from typing import Type, Union
2+
3+
import pytest
4+
from pydantic import VERSION, BaseModel, ValidationError
5+
from pydantic import Field as PField
6+
from sqlmodel import Field, SQLModel
7+
8+
9+
# -----------------------------------------------------------------------------------
10+
# Models
11+
12+
13+
class PydanticUser(BaseModel):
14+
full_name: str = PField(alias="fullName")
15+
16+
17+
class SQLModelUser(SQLModel):
18+
full_name: str = Field(alias="fullName")
19+
20+
21+
# Models with config (validate_by_name=True)
22+
23+
24+
if VERSION.startswith("2."):
25+
26+
class PydanticUserWithConfig(PydanticUser):
27+
model_config = {"validate_by_name": True}
28+
29+
class SQLModelUserWithConfig(SQLModelUser):
30+
model_config = {"validate_by_name": True}
31+
32+
else:
33+
34+
class PydanticUserWithConfig(PydanticUser):
35+
class Config:
36+
allow_population_by_field_name = True
37+
38+
class SQLModelUserWithConfig(SQLModelUser):
39+
class Config:
40+
allow_population_by_field_name = True
41+
42+
43+
# -----------------------------------------------------------------------------------
44+
# Tests
45+
46+
# Test validate by name
47+
48+
49+
@pytest.mark.parametrize("model", [PydanticUser, SQLModelUser])
50+
def test_create_with_field_name(model: Union[Type[PydanticUser], Type[SQLModelUser]]):
51+
with pytest.raises(ValidationError):
52+
model(full_name="Alice")
53+
54+
55+
@pytest.mark.parametrize("model", [PydanticUserWithConfig, SQLModelUserWithConfig])
56+
def test_create_with_field_name_with_config(
57+
model: Union[Type[PydanticUserWithConfig], Type[SQLModelUserWithConfig]],
58+
):
59+
user = model(full_name="Alice")
60+
assert user.full_name == "Alice"
61+
62+
63+
# Test validate by alias
64+
65+
66+
@pytest.mark.parametrize(
67+
"model",
68+
[PydanticUser, SQLModelUser, PydanticUserWithConfig, SQLModelUserWithConfig],
69+
)
70+
def test_create_with_alias(
71+
model: Union[
72+
Type[PydanticUser],
73+
Type[SQLModelUser],
74+
Type[PydanticUserWithConfig],
75+
Type[SQLModelUserWithConfig],
76+
],
77+
):
78+
user = model(fullName="Bob") # using alias
79+
assert user.full_name == "Bob"
80+
81+
82+
# Test validate by name and alias
83+
84+
85+
@pytest.mark.parametrize("model", [PydanticUserWithConfig, SQLModelUserWithConfig])
86+
def test_create_with_both_prefers_alias(
87+
model: Union[Type[PydanticUserWithConfig], Type[SQLModelUserWithConfig]],
88+
):
89+
user = model(full_name="IGNORED", fullName="Charlie")
90+
assert user.full_name == "Charlie" # alias should take precedence
91+
92+
93+
# Test serialize
94+
95+
96+
@pytest.mark.parametrize("model", [PydanticUser, SQLModelUser])
97+
def test_dict_default_uses_field_names(
98+
model: Union[Type[PydanticUser], Type[SQLModelUser]],
99+
):
100+
user = model(fullName="Dana")
101+
data = user.dict()
102+
assert "full_name" in data
103+
assert "fullName" not in data
104+
assert data["full_name"] == "Dana"
105+
106+
107+
# Test serialize by alias
108+
109+
110+
@pytest.mark.parametrize("model", [PydanticUser, SQLModelUser])
111+
def test_dict_default_uses_aliases(
112+
model: Union[Type[PydanticUser], Type[SQLModelUser]],
113+
):
114+
user = model(fullName="Dana")
115+
data = user.dict(by_alias=True)
116+
assert "fullName" in data
117+
assert "full_name" not in data
118+
assert data["fullName"] == "Dana"
119+
120+
121+
# Test json by alias
122+
123+
124+
@pytest.mark.parametrize("model", [PydanticUser, SQLModelUser])
125+
def test_json_by_alias(
126+
model: Union[Type[PydanticUser], Type[SQLModelUser]],
127+
):
128+
user = model(fullName="Frank")
129+
json_data = user.json(by_alias=True)
130+
assert ('"fullName":"Frank"' in json_data) or ('"fullName": "Frank"' in json_data)
131+
assert "full_name" not in json_data
132+
133+
134+
class PydanticUserV2(BaseModel):
135+
first_name: str = PField(
136+
validation_alias="firstName", serialization_alias="f_name"
137+
)
138+
139+
140+
class SQLModelUserV2(SQLModel):
141+
first_name: str = Field(
142+
validation_alias="firstName", serialization_alias="f_name"
143+
)
144+
145+
146+
@pytest.mark.parametrize("model", [PydanticUserV2, SQLModelUserV2])
147+
def test_create_with_validation_alias(model: Union[Type[PydanticUserV2], Type[SQLModelUserV2]]):
148+
user = model(firstName="John")
149+
assert user.first_name == "John"
150+
151+
152+
@pytest.mark.parametrize("model", [PydanticUserV2, SQLModelUserV2])
153+
def test_serialize_with_serialization_alias(
154+
model: Union[Type[PydanticUserV2], Type[SQLModelUserV2]]
155+
):
156+
user = model(firstName="Jane")
157+
data = user.dict(by_alias=True)
158+
assert "f_name" in data
159+
assert "firstName" not in data
160+
assert "first_name" not in data
161+
assert data["f_name"] == "Jane"

0 commit comments

Comments
 (0)