]> git.ipfire.org Git - thirdparty/fastapi/sqlmodel.git/commitdiff
Add `coerce_numbers_to_str` param to Field, add tests
authorYurii Motov <yurii.motov.monte@gmail.com>
Wed, 28 Jan 2026 15:08:03 +0000 (16:08 +0100)
committerYurii Motov <yurii.motov.monte@gmail.com>
Wed, 28 Jan 2026 15:36:54 +0000 (16:36 +0100)
sqlmodel/main.py
tests/test_pydantic/test_field.py

index 84478f24cf8ca9b6fecabc146aff45c1c5acf64f..82a957103049617afcaefb5450fb11f0cdee23d2 100644 (file)
@@ -3,6 +3,7 @@ from __future__ import annotations
 import builtins
 import ipaddress
 import uuid
+import warnings
 import weakref
 from collections.abc import Mapping, Sequence, Set
 from datetime import date, datetime, time, timedelta
@@ -214,6 +215,7 @@ def Field(
     exclude: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None,
     include: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None,
     const: Optional[bool] = None,
+    coerce_numbers_to_str: Optional[bool] = None,
     gt: Optional[float] = None,
     ge: Optional[float] = None,
     lt: Optional[float] = None,
@@ -257,6 +259,7 @@ def Field(
     exclude: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None,
     include: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None,
     const: Optional[bool] = None,
+    coerce_numbers_to_str: Optional[bool] = None,
     gt: Optional[float] = None,
     ge: Optional[float] = None,
     lt: Optional[float] = None,
@@ -309,6 +312,7 @@ def Field(
     exclude: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None,
     include: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None,
     const: Optional[bool] = None,
+    coerce_numbers_to_str: Optional[bool] = None,
     gt: Optional[float] = None,
     ge: Optional[float] = None,
     lt: Optional[float] = None,
@@ -342,6 +346,7 @@ def Field(
     exclude: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None,
     include: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None,
     const: Optional[bool] = None,
+    coerce_numbers_to_str: Optional[bool] = None,
     gt: Optional[float] = None,
     ge: Optional[float] = None,
     lt: Optional[float] = None,
@@ -371,9 +376,18 @@ def Field(
     schema_extra: Optional[dict[str, Any]] = None,
 ) -> Any:
     current_schema_extra = schema_extra or {}
+
+    for param_name in ("coerce_numbers_to_str",):
+        if param_name in current_schema_extra:
+            msg = f"Pass `{param_name}` parameter directly to Field instead of passing it via `schema_extra`"
+            warnings.warn(msg, UserWarning, stacklevel=2)
+
     # 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)
+    current_coerce_numbers_to_str = coerce_numbers_to_str or current_schema_extra.pop(
+        "coerce_numbers_to_str", None
+    )
     field_info_kwargs = {
         "alias": alias,
         "title": title,
@@ -381,6 +395,7 @@ def Field(
         "exclude": exclude,
         "include": include,
         "const": const,
+        "coerce_numbers_to_str": current_coerce_numbers_to_str,
         "gt": gt,
         "ge": ge,
         "lt": lt,
index 140b02fd9b1845d4c515df03828f50e41584a7d5..5484f3f197dcb34e9c54da6b16fc809a4e938fa8 100644 (file)
@@ -54,3 +54,36 @@ def test_repr():
 
     instance = Model(id=123, foo="bar")
     assert "foo=" not in repr(instance)
+
+
+def test_coerce_numbers_to_str_true():
+    class Model(SQLModel):
+        val: str = Field(coerce_numbers_to_str=True)
+
+    assert Model.model_validate({"val": 123}).val == "123"
+    assert Model.model_validate({"val": 45.67}).val == "45.67"
+
+
+@pytest.mark.parametrize("coerce_numbers_to_str", [None, False])
+def test_coerce_numbers_to_str_false(coerce_numbers_to_str: Optional[bool]):
+    class Model2(SQLModel):
+        val: str = Field(coerce_numbers_to_str=coerce_numbers_to_str)
+
+    with pytest.raises(ValidationError):
+        Model2.model_validate({"val": 123})
+
+
+def test_coerce_numbers_to_str_via_schema_extra():  # Current workaround. Remove after some time
+    with pytest.warns(
+        UserWarning,
+        match=(
+            "Pass `coerce_numbers_to_str` parameter directly to Field instead of passing "
+            "it via `schema_extra`"
+        ),
+    ):
+
+        class Model(SQLModel):
+            val: str = Field(schema_extra={"coerce_numbers_to_str": True})
+
+    assert Model.model_validate({"val": 123}).val == "123"
+    assert Model.model_validate({"val": 45.67}).val == "45.67"