Repository: pydantic/pydantic

Issue:
V2 - Validating int->str
Continue of the #5993

```Python
class Data(BaseModel):
    s: str
    f: str
    d: str


print(Data(s=42, f=4.2, d=Decimal('4.2')))
```

this gives validation error that `s f d - Input should be a valid string`

this was working in pydantic v1, and was actually very handy when dealing with parsing data of a public API where users send messy data from JS client sides

so would be nice to have some feature to be able to turn on str parsing (globally?)



### Affected Components

- [X] [Data validation/parsing](https://docs.pydantic.dev/usage/models/#basic-model-usage)


**Update**: https://github.com/pydantic/pydantic/issues/6045#issuecomment-1704943954


Discussion:
It works fine for me.

But,

from pydantic import BaseModel

class User(BaseModel):
    id: int
    name = 'Jane Doe'
    
user = User(id='123')
user_x = User(id='123.45')

assert user.id == 123
assert user_x.id == 123
assert isinstance(user_x.id, int)

Gives me an Error:

`pydantic.error_wrappers.ValidationError: 1 validation error for User
id
  value is not a valid integer (type=type_error.integer)`
  
 It supposed to conver "123.45" to 123
   
`extra='allow'` could be an alternative here for now. it won't convert data to `str` but at least it will allow storing anything without applying any schema.
I really don't think it makes sense as the default in V2 to coerce numbers to strings. Coercing strings to field types is a very special and very common case, it doesn't mean we should support the reverse.

See [this bit of the V2 plan](https://docs.pydantic.dev/latest/blog/pydantic-v2/#formalised-conversion-table) for more details.

The only thing I'd be willing to accept would be a compatibility shim (perhaps set via `config`) that enables converting numbers to strings to be more compatible with V1.
But not many people have asked about it, the one person I've spoken to who mentioned this basically said "the new behaviour is right, the only thing that confused me was the documentation being wrong".

We'd need more demand for this before adding the shim.
@samuelcolvin 

yeah, well that makes some sense, but my feeling is this will be pop up once it get released to wider audience

like imagine you used pydantic to parse some yaml:

```Python
import yaml
from pydantic import BaseModel

YAML = """
details:
  name: 12345
  description: Hello world
 
"""


class Details(BaseModel):
    name: str
    description: str


raw_data = yaml.safe_load(YAML)
print(raw_data)

data = Details(**raw_data["details"])
```

I do not see any nice way to handle this except change ALL `str` field to some special string type
@vitalik just to be clear, we'd be able to get it to behave the old way (i.e., converting `int`s to `str`s, etc.) through just an annotation (i.e., changing the type hint from `str` to `Annotated[str, LenientStr()]` or something like that). Yes, you'd need to add the annotation everywhere in your code, but it would at least not be treated as a different type by type checkers. Not sure if that helps anything, just pointing it out since you said "some special string type" — not sure if your concern was that it would be a different type, or just that it would require changes everywhere (in the latter case, this doesn't help).

That said, I do still understand the interest in a "global" flag. (But I also understand Samuel's point about wanting to see more concrete demand first.)
> but it would at least not be treated as a different type by type checkers

well that's my issue here - as you can see in my example i'm not initialising model with arguments (so type checkers do not play here) 

I'm passing raw yaml data just to parse and validate user input

Basically, I'm inspecting an impact of one of my clients where data parsing is heavily done with pydantic and migrating to v2 could simply lead to validation errors for data that was fine before

Changing all the code from `:str` to `:Annotated[str, force]` would be a really painful and annoying process, so it would be nice if you could provide some hook for easy monkey patching this behaviour (at least on a class level)
@adriangb I’m wondering if we could override the get_pydantic_core_schema or one of the other magic methods and have it insert annotations for certain types? I guess maybe a prepare pydantic annotations for a BaseModel class? Not sure if that would work on the class level instead of the field level but I’m thinking there might be something here that makes this “configurable” even without the use of config
I think we can create or should be able to make changes so we can create a custom type like `LaxInt = Annotated[int, ...]`. The thing is that users would have to explicitly use that everywhere. I'm also not sure it would work right now, but we can (and should) re-factor internally to make it work.

We have also discussed about making a hook to do some sort of global replacement `{int: LaxInt}` but that has consequences for references and that sort of thing, so it probably needs a little bit more thought, even if it seems simple to implement.
Right I was mostly curious if there might be a way to hook into schema generation for models specifically as a way to handle this. I’ll play with it a bit this afternoon and see if I can find anything that works without code changes today, or at least see how far it is from working.
So yeah you can do this:

```python
from typing import Annotated

from pydantic import BeforeValidator, TypeAdapter

LaxStr = Annotated[str, BeforeValidator(lambda x: str(x))]

ta = TypeAdapter(LaxStr)

print(ta.validate_python(123))
```

(Sorry about any confusion above about `LaxInt`)

But there's no way to set this globally or recursively for a model.

You could write a function that recursively inspects a `CoreSchema` and wraps any `{'type': 'str', ...}` schema with the validator above, then you could make that part of an annotation (`Annotated[T, RecursivelyMakeStrLax]`), a base class (`class LaxStrBaseModel(BaseModel): ... def __get_pydantic_core_schema__(...)`) or something like that.
Yeah that’s what I was thinking. (Though I hadn’t fleshed it out as much, that’s helpful.)

I think that might be enough even if it didn’t do any nested model recursion (though it might need to recurse on core schemas for the purpose of interacting properly with other validators etc., at which point maybe it’s easier to do it fully recursive) since the hypothetical user could just use that as the base class for all their models.
> So yeah you can do this:

Caution
```Python
Annotated[str, BeforeValidator(lambda x: str(x))]

ta.validate_python(None) # will result -> "None" (as type str)
```
Probably want to use `pydantic.v1.validators.str_validator` or whatever the function was from v1
I am currently migrating v1 -> v2 and am facing issues with this. It would be great to at least have the option to set this in the model configs.

The use case I have for coercing int -> str is we often have IDs that appear as numericals (e.g. 12839384) and are infered as such when using pandas, whereas we want to treat them as string in order to perform string manipulation or join them with other non-numerical IDs. Pydantic v1 allowed me not to worry about this, whereas now I have to coerce all of my IDs separately beforehand.

May just add that I don't really see the benefit of removing this original behavior from v1, and that it will probably cause a lot of headaches when migrating, especially that this is not documented clearly.
I think this could be another case for a type map we've discussed privately. As far as I remember one of the ideas was to have a global registry that will map an annotation to another one. It could look from a user point of view as something like this:

```py
type_map_registry: dict[type, type] = {str: Annotated[str, Field(force=True)]}
```

This would force all `str` fields in all modules to be treated as `Annotated[str, Field(force=True)]`.
@mrenner-ML Indeed, this isn't stated explicitly in the migration guide. In general, coercing numbers and more complex types to `str` just hides errors and virtually disables the type checking. Overall, Pydantic V2 is more strict than Pydantic V1. However, there are more ways for customizing behavior in Pydantic V2.

In Pydantic V2 it's possible to achieve the desired behavior using [Custom Data Types](https://docs.pydantic.dev/dev-v2/usage/types/custom/).
> It could look from a user point of view as something like this:
> 
> ```python
> type_map_registry: dict[type, type] = {str: Annotated[str, Field(force=True)]}
> ```

yeah, that should work for all of my cases... would also be nice to have this global and per base model class and a concrete model
@vitalik I'm not sure when such a type map could be implemented. However, PRs are always welcome:)
I can take a look, could you give some idea which module/class this best to add this functionality ?
I think, `pydantic/main.py` is a good place to start. This is where `BaseModel` lives. However, it might lead to `pydantic/_internal/_model_construction.py` where `ModelMetaclass` is or to `pydantic/_internal/_generate_schema.py` where schema is being generated and actual schema variants are being chosen.
with #6535 it would be possible to change annotation behaviour per model or even global 

```Python
BaseModel.model_config['replace_types'] = {str: LaxStr}
```
@adriangb @dmontagu 

patching FieldInfo annotation does not really work...

see the following example - is this a bug or expected ?


```Python
from typing import Annotated
from pydantic.v1.validators import str_validator

LaxStr = Annotated[str, BeforeValidator(str_validator)]

class Data(BaseModel):
    gender: LaxStr = Field('F', max_length=1)


print(Data(gender='M'))
```
gives
```
gender
  Value should have at most 1 item after validation, not 1 [type=too_long, input_value='M', input_type=str]
    For further information visit https://errors.pydantic.dev/2.1.2/v/too_long
```
Hey guys :wave: 

I encounter the same issue migrating from v1 to v2. 

I am using Pydantic to build an XML later on so everything needs to be a String but also I need to support Ids to use SnowflakeIDs and Javascript can not handle them so I need to return them as strings

I know it can be painful to go on the code and update the str to the custom type but it is the only valid solution that I could find because I also agree that it should not be coerced anymore by default


```
from typing import Annotated

from pydantic import (
    PlainSerializer,
    WithJsonSchema,
)

# Represents an integer value that is serialized as a string
IntToStr = Annotated[
    int,
    PlainSerializer(lambda x: f"{x}", return_type=str),
    WithJsonSchema({"type": "string"}, mode="serialization"),
]

class MyAwesomePydanticSchema(BaseModel):
    id: IntToStr = Field(
        description="The corresponding ID",
        example=42,
        ge=1,
    )
```

Probably on the next iteration we can already include that on the library or add a property that can facilitate the usage

I am testing this at the moment and so far so good but if you have any opinion/place to improve let me know
@vitalik that example seems to work on `v2.0.3`:

```python
from typing import Annotated

from pydantic import BaseModel, BeforeValidator, Field
from pydantic.v1.validators import str_validator

LaxStr = Annotated[str, BeforeValidator(str_validator)]

class Data(BaseModel):
    gender: LaxStr = Field('F', max_length=1)


print(Data(gender='M'))
#> gender='M'
```

Given that we've offered a solution for this particular issue using the `LaxStr` approach, @vitalik can we close this and continue discussion of a `replace_types` like approach in #6535 ?
well let's keep it open as it already added to milestone 
Is there a follow up ticket to fix the documentation?

Currently [the page about strict mode](https://docs.pydantic.dev/latest/usage/strict_mode/) says:

>By default, Pydantic will attempt to coerce values to the desired type when possible. For example, you can pass the string "123" as the input to an int field, and it will be converted to 123. This coercion behavior is useful in many scenarios — think: UUIDs, URL parameters, HTTP headers, environment variables, user input, etc.

>However, there are also situations where this is not desirable, and you want Pydantic to error instead of coercing data.

>To better support this use case, Pydantic provides a "strict mode" that can be enabled on a per-model, per-field, or even per-validation-call basis. When strict mode is enabled, Pydantic will be much less lenient when coercing data, and will instead error if the data is not of the correct type.

How would one explain it better? A good smell for how a feature should work is to figure out the docs, and if the docs need to put in difficult to explain exceptions for how things work (like what's needed here) it likely means the exception is not a good idea. In this case, being unable to turn off strict mode for `str` seems like a weird thing to try to explain.

Perhaps:

>To better support this use case, Pydantic provides a "strict mode" that can be enabled on a per-model, per-field, or even per-validation-call basis. When strict mode is enabled, Pydantic will be much less lenient when coercing data, and will instead error if the data is not of the correct type. In our experiences, this is so important for parsing strings, pydantic will use strict on `str` types by default as an exceptional case. To disable this for strings, explicitly disable the strict setting on the field.
@ehiggs Could you please submit a PR for the docs?
For anyone coming to this issue, as of version 2.1 this is the way to make it work for all models by default

```
from pydantic import BaseModel, ConfigDict, GenerateSchema
from pydantic_core import CoreSchema, core_schema


class LaxStrGenerator(GenerateSchema):
    def str_schema(self) -> CoreSchema:
        return core_schema.no_info_plain_validator_function(str)


BaseModel.model_config = ConfigDict(schema_generator=LaxStrGenerator)
```
Yes, but be careful monkey patching `BaseModel.model_config`. Obviously if more than one thing does it won't work.
Hi, I'm not sure if this is intentional or not, but this doesn't seem to work with `pattern`. Here's an example:
```py
from pydantic import BaseModel, ConfigDict, Field, GenerateSchema
from pydantic_core import CoreSchema, core_schema


class LaxStrGenerator(GenerateSchema):
    def str_schema(self) -> CoreSchema:
        return core_schema.no_info_plain_validator_function(str)


class CustomBaseModel(BaseModel):
    model_config = ConfigDict(schema_generator=LaxStrGenerator)


class TestModel(CustomBaseModel):
    test: str = Field(min_length=1, max_length=32, pattern=r'^[a-zA-Z0-9 ]+$')
```
Which yields:
```
Traceback (most recent call last):
  File "/test.py", line 14, in <module>
    class TestModel(CustomBaseModel):
  File "/.venv/lib/python3.11/site-packages/pydantic/_internal/_model_construction.py", line 173, in __new__
    complete_model_class(
  File "/.venv/lib/python3.11/site-packages/pydantic/_internal/_model_construction.py", line 481, in complete_model_class
    cls.__pydantic_validator__ = SchemaValidator(simplified_core_schema, core_config)
                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.SchemaError: Invalid Schema:
model.schema.model-fields.fields.test.schema.function-after.pattern
  Extra inputs are not permitted [type=extra_forbidden, input_value='^[a-zA-Z0-9 ]+$', input_type=str]
    For further information visit https://errors.pydantic.dev/2.1/v/extra_forbidden
```
Thanks!
Try `core_schema.no_info_before_validator(str, core_schema.str_schema())`
I seem to have the same problem with that 🤔
```py
class LaxStrGenerator(GenerateSchema):
    def str_schema(self) -> CoreSchema:
        return core_schema.no_info_before_validator_function(str, core_schema.str_schema())
```
This is indeed a bug/limitation. Working on it.
Facing the same issue here when migrating from v1 to v2. Parsing huge excel sheets that I have little control over, and I want to convert some variables to string regardless of them being numbers or strings so I don't change any information when outputting in reports. 

Not clear what the optimal solution of the above options are. Can anyone point me in the least hacky way to do this? I would rather not go back to v1

Would be excellent if one could set an option to Field that str() should be tried on the value 
@wholmen Using `LaxStr = Annotated[str, BeforeValidator(lambda x: str(x))]` in place of `str`  in models where you want to cast int to str is the current solution, although you might also have luck with `LaxStrGenerator` above—unless you use `pattern`, until #6951 is merged.
What is the way to do this in 2.3.0 with pattern? Thank you!
There seems to be another instance of documentation describing old behavior: https://docs.pydantic.dev/latest/usage/types/standard_types/ (https://github.com/pydantic/pydantic/blob/main/docs/usage/types/standard_types.md?plain=1#L14):

> Strings are accepted as-is. `int`, `float`, and `Decimal` are coerced using `str(v)`.
Solution: ` data_id: Union[int, str]`
It would be great to have this as a configuration that pydantic loads from the environment/file.

The reason is that patching BaseModel this way has the disadvantage that it must be done before importing any module that uses BaseModel, which adds to the mental load and is easy to mess up without noticing.

Another possible solution is implementing a model validator in a base class that would replace BaseModel. This also has the disadvantage of having yet another thing to remember.
@David-OConnor It might cause things break unexpectedly down the line if code using the model is not prepared to handle int. For example `print('Data id is:' + data.data_id)` raises TypeError if `data_id` value is integer.
I've re-opened this. On balance I think we should add a config flag to re-instate the V1 behavior since grander changes to allow type replacement is going to take much longer.
> @David-OConnor It might cause things break unexpectedly down the line if code using the model is not prepared to handle int. For example `print('Data id is:' + data.data_id)` raises TypeError if `data_id` value is integer.

The case I'm referring to is parsing an int or string-of-int into int. So, downstream only sees and handles ints.
Edit: Updating to 2.3.0 fixed this issue.

---

I'm unsure if this is the same issue, but I'll post the issue I'm having here also. The data should be `str` throughout. If someone believes this is a different issue (or if I'm just doing this completely wrong) please let me know.

```py
StrippedStr = Annotated[
    str,
    BeforeValidator(lambda v: v.strip() if isinstance(v, str) else v),
]

AlphaStr = Annotated[
    StrippedStr,
    Field(pattern=r"^[a-zA-Z]+$"),
]


class Data(BaseModel):
    a: AlphaStr  # works
    b: AlphaStr | None  # does not work (see error below)
```

```
  File "site-packages\pydantic\_internal\_model_construction.py", line 173, in __new__
    complete_model_class(
  File "site-packages\pydantic\_internal\_model_construction.py", line 481, in complete_model_class
    cls.__pydantic_validator__ = SchemaValidator(simplified_core_schema, core_config)
                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.SchemaError: Invalid Schema:
model.schema.model-fields.fields.a.schema.default.schema.nullable.schema.function-before.pattern
  Extra inputs are not permitted [type=extra_forbidden, input_value='^[a-zA-Z]+$', input_type=str]
    For further information visit https://errors.pydantic.dev/2.1/v/extra_forbidden
```
@CallumAtCarter  I can't reproduce, can you try installing `pydantic` 2.3.0 or from main? I suspect this got fixed in https://github.com/pydantic/pydantic/pull/6951.

```python
from typing import Annotated

from pydantic import BaseModel, Field, BeforeValidator


StrippedStr = Annotated[
    str,
    BeforeValidator(lambda v: v.strip() if isinstance(v, str) else v),
]

AlphaStr = Annotated[
    StrippedStr,
    Field(pattern=r"^[a-zA-Z]+$"),
]


class Data(BaseModel):
    a: AlphaStr  # works
    b: AlphaStr | None  # does not work (see error below)


Data(a="abc", b=None)
Data(a="abc", b="  def  ")
Data(a="abc", b="!!!")
"""
b
  String should match pattern '^[a-zA-Z]+$' [type=string_pattern_mismatch, input_value='!!!', input_type=str]
    For further information visit https://errors.pydantic.dev/2.3/v/string_pattern_mismatch
"""
````

But yes this is unrelated to this issue so if that doesn't fix it for you please open a new discussion (and feel free to tag me).
@adriangb I should have updated before posting 😔. Updating to 2.3.0 fixed it, thanks!

Resolving patch:
diff --git a/pydantic/_internal/_config.py b/pydantic/_internal/_config.py
--- a/pydantic/_internal/_config.py
+++ b/pydantic/_internal/_config.py
@@ -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:
@@ -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
@@ -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,
 )
 
 
diff --git a/pydantic/config.py b/pydantic/config.py
--- a/pydantic/config.py
+++ b/pydantic/config.py
@@ -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__)


Test patch:
diff --git a/tests/test_types.py b/tests/test_types.py
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -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,
@@ -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}