Repository: pydantic/pydantic

Issue:
Parsing JSON number to Decimal loose precision
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

I believe PR #6327 introduced Decimal validator regression.

https://github.com/pydantic/pydantic/blob/6ef959fa69d1ab1aedf2778b6c8f452e0fee37b6/pydantic/_internal/_std_types_schema.py#L189-L194
Such a schema union causes that in the first place the JSON literal data is cast to the float type. This in turn causes a loss of accuracy upon subsequent conversion to Decimal.

Attached script results are:

v2.0.0
```
python -> a=Decimal('0.1234')
json -> a=Decimal('0.12339999999999999580335696691690827719867229461669921875')
```
This result was reported as #6295 fixed by #6321

v2.0.2
```
python -> a=Decimal('0.1234')
json -> a=Decimal('0.1234')
```
I believe this result as a correct behaviour, after #6321 is merged. 

v2.0.3
```
python -> a=Decimal('0.12339999999999999580335696691690827719867229461669921875')
json -> a=Decimal('0.12339999999999999580335696691690827719867229461669921875')
```
This result is actual behaviour with v2.0.3 installed, after #6327 merged. Validation logic was moved to pydantic-core in order to achieve speedup, but precision lost is introduced as a side effect.

According to Contribution Guide I'm writing this issue, but I think that I'm also able to start PR to fix it. It should be simple as removing single
https://github.com/pydantic/pydantic/blob/6ef959fa69d1ab1aedf2778b6c8f452e0fee37b6/pydantic/_internal/_std_types_schema.py#L191
to avoid conversion to float and result in the same validation logic as in v2.0.2
EDIT: Also `str_schema` `strict` param should be set to `False`.


### Example Code

```Python
from decimal import Decimal
from pydantic import BaseModel

class TestModel(BaseModel):
    a: Decimal

data = {"a": 0.1234}
data_json = '{"a":0.1234}'

print('python ->',TestModel.model_validate(data))
print('json ->',TestModel.model_validate_json(data_json))
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.0.3
        pydantic-core version: 2.3.0 release build profile
                 install path: /home/kuba/pydanticDecimal/venv/lib/python3.11/site-packages/pydantic
               python version: 3.11.3 (main, Apr  5 2023, 14:15:06) [GCC 9.4.0]
                     platform: Linux-5.15.0-75-generic-x86_64-with-glibc2.31
     optional deps. installed: ['typing-extensions']
```


Selected Assignee: @Kludex


Discussion:
I think you are right. We need to always convert it to a string even if it is a float because the very act of parsing the json float string into a Rust or Python float causes precision loss. I'll make a PR to fix this.

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
@@ -180,7 +180,7 @@ def __get_pydantic_json_schema__(self, _schema: CoreSchema, handler: GetJsonSche
     def __get_pydantic_core_schema__(self, _source_type: Any, _handler: GetCoreSchemaHandler) -> CoreSchema:
         Decimal = decimal.Decimal
 
-        def to_decimal(v: Any) -> decimal.Decimal:
+        def to_decimal(v: str) -> decimal.Decimal:
             try:
                 return Decimal(v)
             except decimal.DecimalException as e:
@@ -188,9 +188,13 @@ def to_decimal(v: Any) -> decimal.Decimal:
 
         primitive_schema = core_schema.union_schema(
             [
-                core_schema.float_schema(strict=True),
+                # if it's an int keep it like that and pass it straight to Decimal
+                # but if it's not make it a string
+                # we don't use JSON -> float because parsing to any float will cause
+                # loss of precision
                 core_schema.int_schema(strict=True),
                 core_schema.str_schema(strict=True, strip_whitespace=True),
+                core_schema.no_info_plain_validator_function(str),
             ],
         )
         json_schema = core_schema.no_info_after_validator_function(to_decimal, primitive_schema)


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
@@ -446,7 +446,7 @@ class Model(BaseModel):
         },
         {
             'type': 'decimal_parsing',
-            'loc': ('v', 2, 'function-after[to_decimal(), union[float,int,constrained-str]]'),
+            'loc': ('v', 2, 'function-after[to_decimal(), union[int,constrained-str,function-plain[str()]]]'),
             'msg': 'Input should be a valid decimal',
             'input': 'x',
         },
@@ -1237,9 +1237,7 @@ class Model(BaseModel):
             'type': 'is_instance_of',
             'loc': (
                 'a',
-                'function-after[check_digits_validator(), json-or-python[json=function-after[to_decimal(), '
-                'union[float,int,constrained-str]],python=lax-or-strict[lax=union[is-instance[Decimal],'
-                'function-after[to_decimal(), union[float,int,constrained-str]]],strict=is-instance[Decimal]]]]',
+                'function-after[check_digits_validator(), json-or-python[json=function-after[to_decimal(), union[int,constrained-str,function-plain[str()]]],python=lax-or-strict[lax=union[is-instance[Decimal],function-after[to_decimal(), union[int,constrained-str,function-plain[str()]]]],strict=is-instance[Decimal]]]]',
                 'is-instance[Decimal]',
             ),
             'msg': 'Input should be an instance of Decimal',
@@ -1250,10 +1248,8 @@ class Model(BaseModel):
             'type': 'decimal_parsing',
             'loc': (
                 'a',
-                'function-after[check_digits_validator(), json-or-python[json=function-after[to_decimal(), '
-                'union[float,int,constrained-str]],python=lax-or-strict[lax=union[is-instance[Decimal],'
-                'function-after[to_decimal(), union[float,int,constrained-str]]],strict=is-instance[Decimal]]]]',
-                'function-after[to_decimal(), union[float,int,constrained-str]]',
+                'function-after[check_digits_validator(), json-or-python[json=function-after[to_decimal(), union[int,constrained-str,function-plain[str()]]],python=lax-or-strict[lax=union[is-instance[Decimal],function-after[to_decimal(), union[int,constrained-str,function-plain[str()]]]],strict=is-instance[Decimal]]]]',
+                'function-after[to_decimal(), union[int,constrained-str,function-plain[str()]]]',
             ),
             '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
@@ -1292,7 +1292,7 @@ def __bool__(self) -> bool:
         ('uuid_check', b'\x12\x34\x56\x78' * 4, UUID('12345678-1234-5678-1234-567812345678')),
         ('uuid_check', 'ebcdab58-6eb8-46fb-a190-', ValidationError),
         ('uuid_check', 123, ValidationError),
-        ('decimal_check', 42.24, Decimal(42.24)),
+        ('decimal_check', 42.24, Decimal('42.24')),
         ('decimal_check', '42.24', Decimal('42.24')),
         ('decimal_check', b'42.24', ValidationError),
         ('decimal_check', '  42.24  ', Decimal('42.24')),
@@ -5615,3 +5615,14 @@ def test_string_constraints() -> None:
         Annotated[str, StringConstraints(strip_whitespace=True, to_lower=True), AfterValidator(lambda x: x * 2)]
     )
     assert ta.validate_python(' ABC ') == 'abcabc'
+
+
+def test_decimal_float_precision() -> None:
+    """https://github.com/pydantic/pydantic/issues/6807"""
+    ta = TypeAdapter(Decimal)
+    assert ta.validate_json('1.1') == Decimal('1.1')
+    assert ta.validate_python(1.1) == Decimal('1.1')
+    assert ta.validate_json('"1.1"') == Decimal('1.1')
+    assert ta.validate_python('1.1') == Decimal('1.1')
+    assert ta.validate_json('1') == Decimal('1')
+    assert ta.validate_python(1) == Decimal('1')