From: Yurii Motov Date: Wed, 28 Jan 2026 15:08:03 +0000 (+0100) Subject: Add `coerce_numbers_to_str` param to Field, add tests X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=51aefc7987a99c98589724f7c6cba3653f89dec5;p=thirdparty%2Ffastapi%2Fsqlmodel.git Add `coerce_numbers_to_str` param to Field, add tests --- diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 84478f24..82a95710 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -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, diff --git a/tests/test_pydantic/test_field.py b/tests/test_pydantic/test_field.py index 140b02fd..5484f3f1 100644 --- a/tests/test_pydantic/test_field.py +++ b/tests/test_pydantic/test_field.py @@ -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"