Skip to content

Commit

Permalink
✨ Implement optional number to str coercion
Browse files Browse the repository at this point in the history
  • Loading branch information
lig committed Sep 20, 2023
1 parent 7bbf010 commit 2b12488
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 0 deletions.
3 changes: 3 additions & 0 deletions pydantic/_internal/_config.py
Expand Up @@ -78,6 +78,7 @@ class ConfigWrapper:
schema_generator: type[GenerateSchema] | None
json_schema_serialization_defaults_required: bool
json_schema_mode_override: Literal['validation', 'serialization', None]
coerce_numbers_to_str: bool

def __init__(self, config: ConfigDict | dict[str, Any] | type[Any] | None, *, check: bool = True):
if check:
Expand Down Expand Up @@ -173,6 +174,7 @@ def dict_not_none(**kwargs: Any) -> Any:
str_max_length=self.config_dict.get('str_max_length'),
str_min_length=self.config_dict.get('str_min_length'),
hide_input_in_errors=self.config_dict.get('hide_input_in_errors'),
coerce_numbers_to_str=self.config_dict.get('coerce_numbers_to_str'),
)
)
return core_config
Expand Down Expand Up @@ -243,6 +245,7 @@ def _context_manager() -> Iterator[None]:
schema_generator=None,
json_schema_serialization_defaults_required=False,
json_schema_mode_override=None,
coerce_numbers_to_str=False,
)


Expand Down
7 changes: 7 additions & 0 deletions pydantic/config.py
Expand Up @@ -230,5 +230,12 @@ class without an annotation and has a type that is not in this tuple (or otherwi
Defaults to `None`.
"""

coerce_numbers_to_str: bool
"""
If `True`, enables automatic coercion of any `Number` type to `str` in "lax" (non-strict) mode.
Defaults to `False`.
"""


__getattr__ = getattr_migration(__name__)
44 changes: 44 additions & 0 deletions tests/test_types.py
Expand Up @@ -12,6 +12,7 @@
from datetime import date, datetime, time, timedelta, timezone
from decimal import Decimal
from enum import Enum, IntEnum
from numbers import Number
from pathlib import Path
from typing import (
Any,
Expand Down Expand Up @@ -5870,3 +5871,46 @@ def test_decimal_float_precision() -> None:
assert ta.validate_python('1.1') == Decimal('1.1')
assert ta.validate_json('1') == Decimal('1')
assert ta.validate_python(1) == Decimal('1')


def test_coerce_numbers_to_str_disabled_in_strict_mode() -> None:
class Model(BaseModel):
model_config = ConfigDict(strict=True, coerce_numbers_to_str=True)
value: str

with pytest.raises(ValidationError, match='value'):
Model.model_validate({'value': 42})
with pytest.raises(ValidationError, match='value'):
Model.model_validate_json('{"value": 42}')


@pytest.mark.parametrize(
('number', 'expected_str'),
[
pytest.param(42, '42', id='42'),
pytest.param(42.0, '42.0', id='42.0'),
pytest.param(Decimal('42.0'), '42.0', id="Decimal('42.0')"),
],
)
def test_coerce_numbers_to_str(number: Number, expected_str: str) -> None:
class Model(BaseModel):
model_config = ConfigDict(coerce_numbers_to_str=True)
value: str

assert Model.model_validate({'value': number}).model_dump() == {'value': expected_str}


@pytest.mark.parametrize(
('number', 'expected_str'),
[
pytest.param('42', '42', id='42'),
pytest.param('42.0', '42', id='42.0'),
pytest.param('42.13', '42.13', id='42.13'),
],
)
def test_coerce_numbers_to_str_from_json(number: str, expected_str: str) -> None:
class Model(BaseModel):
model_config = ConfigDict(coerce_numbers_to_str=True)
value: str

assert Model.model_validate_json(f'{{"value": {number}}}').model_dump() == {'value': expected_str}

0 comments on commit 2b12488

Please sign in to comment.