Repository: pydantic/pydantic

Issue:
Decimal Deserialization Doesn't Maintain Accuracy
### Initial Checks

- [X] I confirm that I'm using Pydantic V2 installed directly from the `main` branch, or equivalent

### Description

While using FastAPI 0.100.0b1 and Pydantic 2.0b3 I've noticed that decimals deserialized from JSON no longer maintain the accuracy as stated in the [documentation](https://docs.pydantic.dev/dev-v2/usage/types/number_types/#validation-of-numeric-types).

>  Pydantic attempts to convert the value to a string, then passes the string to Decimal(v).

It's clear from simple testing that Pydantic v2 no longer does this, and the behavior matches what would happen if you created a decimal from a float, like so:

```python
decimal.Decimal(48.717983)
```

where the documentation (and v1 behavior) states that the following should happen:

```python
decimal.Decimal("48.717983")
```

This behavior results in decimal fields that do not accurately match the intended value.

### Example Code

```Python
#!/usr/bin/env python

import decimal
import pydantic

data = '{"number":48.717983}'

class Test(pydantic.BaseModel):
    number: decimal.Decimal

parsed = Test.model_validate_json(data)  # use parse_raw for v1
expected = decimal.Decimal("48.717983")
assert parsed.number == expected
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.0b3
        pydantic-core version: 0.42.0 release build profile
                 install path: /Users/stephen/code/pydantic/pydantic
               python version: 3.11.4 (main, Jun 13 2023, 16:13:25) [Clang 14.0.3 (clang-1403.0.22.14.1)]
                     platform: macOS-13.4.1-x86_64-i386-64bit
     optional deps. installed: ['typing-extensions']
```
```


Selected Assignee: @dmontagu


Discussion:
For what it’s worth, you can use a string instead of a number in the JSON to resolve this, but I understand if you can’t control that and agree it’s worth a fix.

@davidhewitt do you know your way around arbitrary precision floating points from JSON using serde? I remember trying to get arbitrary size ints working to handle ints outside the range of i64, but it was a little bit over my head, and I’m not sure if there’s something analogous for floats anyway..

(Also, for a bit clearer view into the sample script:)
```python
#!/usr/bin/env python

import decimal
import pydantic

data = '{"number":48.717983}'

class Test(pydantic.BaseModel):
    number: decimal.Decimal

parsed = Test.model_validate_json(data)  # use parse_raw for v1
expected = decimal.Decimal("48.717983")
print(parsed.number)
#> 48.71798299999999670717443223111331462860107421875
print(expected)
#> 48.717983
assert parsed.number == expected
```
This is promising:

```python
#!/usr/bin/env python

import decimal
from typing import Annotated

import pydantic

data = '{"number":48.717983}'

class Test(pydantic.BaseModel):
    number: Annotated[str, pydantic.PlainValidator(lambda v: str(v)), pydantic.AfterValidator(lambda v: decimal.Decimal(v))]

parsed = Test.model_validate_json(data)
expected = decimal.Decimal("48.717983")
print(parsed.number)
#> 48.717983
print(expected)
#> 48.717983
assert parsed.number == expected
```
I'm thinking maybe we can leverage this to come up with a better pydantic-core schema for Decimal, even without changes to `pydantic-core`.
Thanks for picking this up @dmontagu! We're keeping an eye on this as we pass around decimals frequently in our system to be used within financial calculations. Before we added Pydantic to our toolset, we were very intentional about the way that we handled the serialization and de-serialization of these types. One of the big (among many) positives of adopting Pydantic is that it took the same approach as we did, but out of the box without any extra work.

It's unclear if that approach is changing in Pydantic v2, as the docs conflict themselves.

[v2 Number Types](https://docs.pydantic.dev/dev-v2/usage/types/number_types/#validation-of-numeric-types):

> Pydantic attempts to convert the value to a string, then passes the string to Decimal(v).

[v2 Migration Guide](https://docs.pydantic.dev/dev-v2/migration/#validator-behavior-changes)

> int, float, and decimal values are no longer coerced to strings

If anything, you've at least given us a direction that we can look to pursue if we need to implement our own serialization.
@sdelano

> int, float, and decimal values are no longer coerced to strings

This is a reference to the idea that if you have a `str` field, you can't pass a Decimal:
```python
from decimal import Decimal

from pydantic import BaseModel


class Model(BaseModel):
    x: str


Model(x=Decimal('1.1'))
"""
x
  Input should be a valid string [type=string_type, input_value=Decimal('1.1'), input_type=Decimal]
    For further information visit https://errors.pydantic.dev/2.0.0/v/string_type
"""
```

> Pydantic attempts to convert the value to a string, then passes the string to Decimal(v).

This is saying that if you give a value for a field of type `Decimal`, it will have `str` called on it then `Decimal`. However, in v2, we are a bit more strict about what the inputs are allowed to be (must be a `str`/`int`/`float`/`Decimal`, not an arbitrary value), and this description obviously is not 100% correct with regard to what happens when loading JSON. (Note that my example with `Annotated` above _does_ do exactly this though.)

That said, I've spoken with @samuelcolvin about this, and I think our conclusion is that we just need to add better support for Decimal directly to `pydantic-core`, which would ensure that even when parsing from JSON you will get perfect precision out of numeric types. (Apparently @samuelcolvin already has a trick for this from the `speedate` library so it shouldn't be too difficult.)

That said, we've _just_ released v2.0 and didn't get a fix in for this, but I think this is a great candidate for v2.1 (or perhaps a patch release even sooner). We'll keep you updated here.


Resolving patch:
diff --git a/pydantic/_internal/_std_types_schema.py b/pydantic/_internal/_std_types_schema.py
--- a/pydantic/_internal/_std_types_schema.py
+++ b/pydantic/_internal/_std_types_schema.py
@@ -167,42 +167,20 @@ def __get_pydantic_json_schema__(self, _schema: CoreSchema, handler: GetJsonSche
             return string_schema
 
     def __get_pydantic_core_schema__(self, _source_type: Any, _handler: GetCoreSchemaHandler) -> CoreSchema:
-        primitive_type_union = [
-            core_schema.int_schema(),
-            core_schema.float_schema(),
-            core_schema.str_schema(strip_whitespace=True),
-        ]
-        is_instance_schema = core_schema.json_or_python_schema(
-            json_schema=core_schema.no_info_after_validator_function(
-                decimal.Decimal, core_schema.union_schema(primitive_type_union)
-            ),
-            python_schema=core_schema.is_instance_schema(decimal.Decimal),
-        )
-        lax = core_schema.no_info_after_validator_function(
-            self.validate,
-            core_schema.union_schema(
-                [is_instance_schema, *primitive_type_union],
-                strict=True,
-            ),
-        )
-        strict = core_schema.custom_error_schema(
-            core_schema.no_info_after_validator_function(
-                self.validate,
-                is_instance_schema,
-            ),
-            custom_error_type='decimal_type',
-            custom_error_message='Input should be a valid Decimal instance or decimal string in JSON',
-        )
-        return core_schema.lax_or_strict_schema(
-            lax_schema=lax, strict_schema=strict, serialization=core_schema.to_string_ser_schema()
-        )
-
-    def validate(self, __input_value: int | float | str) -> decimal.Decimal:  # noqa: C901 (ignore complexity)
-        if isinstance(__input_value, decimal.Decimal):
-            value = __input_value
+        return core_schema.general_after_validator_function(self.validate, core_schema.any_schema())
+
+    def validate(  # noqa: C901 (ignore complexity)
+        self, input_value: Any, info: core_schema.ValidationInfo
+    ) -> decimal.Decimal:
+        if isinstance(input_value, decimal.Decimal):
+            value = input_value
+        elif self.strict or (info.config or {}).get('strict', False) and info.mode == 'python':
+            raise PydanticCustomError(
+                'decimal_type', 'Input should be a valid Decimal instance or decimal string in JSON'
+            )
         else:
             try:
-                value = decimal.Decimal(str(__input_value))
+                value = decimal.Decimal(str(input_value))
             except decimal.DecimalException:
                 raise PydanticCustomError('decimal_parsing', 'Input should be a valid decimal')
 
@@ -267,14 +245,6 @@ def validate(self, __input_value: int | float | str) -> decimal.Decimal:  # noqa
                     {'multiple_of': self.multiple_of},
                 )
 
-        # these type checks are here to handle the following error:
-        # Operator ">" not supported for types "(
-        #   <subclass of int and Decimal>
-        #   | <subclass of float and Decimal>
-        #   | <subclass of str and Decimal>
-        #   | Decimal" and "int
-        #   | Decimal"
-        # )
         if self.gt is not None and not value > self.gt:  # type: ignore
             raise PydanticKnownError('greater_than', {'gt': self.gt})
         elif self.ge is not None and not value >= self.ge:  # type: ignore


Test patch:
diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py
--- a/tests/test_edge_cases.py
+++ b/tests/test_edge_cases.py
@@ -1206,6 +1206,7 @@ class Model(BaseModel):
     with pytest.raises(ValidationError) as exc_info:
         Model(a='foobar')
 
+    # insert_assert(exc_info.value.errors(include_url=False))
     assert exc_info.value.errors(include_url=False) == [
         {
             'type': 'int_parsing',
@@ -1221,10 +1222,7 @@ class Model(BaseModel):
         },
         {
             'type': 'decimal_parsing',
-            'loc': (
-                'a',
-                'lax-or-strict[lax=function-after[validate(), union[json-or-python[json=function-after[Decimal(), union[int,float,constrained-str]],python=is-instance[Decimal]],int,float,constrained-str]],strict=custom-error[function-after[validate(), json-or-python[json=function-after[Decimal(), union[int,float,constrained-str]],python=is-instance[Decimal]]]]]',  # noqa: E501
-            ),
+            'loc': ('a', 'function-after[validate(), any]'),
             'msg': 'Input should be a valid decimal',
             'input': 'foobar',
         },
diff --git a/tests/test_types.py b/tests/test_types.py
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -1039,6 +1039,18 @@ class Model(BaseModel):
     assert Model(v=v).v == v
     assert Model(v=v).model_dump() == {'v': v}
 
+    assert Model.model_validate_json('{"v": "1.23"}').v == Decimal('1.23')
+
+
+def test_decimal_precision() -> None:
+    ta = TypeAdapter(Decimal)
+
+    num = f'{1234567890 * 100}.{1234567890 * 100}'
+
+    expected = Decimal(num)
+    assert ta.validate_python(num) == expected
+    assert ta.validate_json(f'"{num}"') == expected
+
 
 def test_strict_date():
     class Model(BaseModel):