Skip to content

Commit

Permalink
fix #2293: Properly encode Decimals without any decimal places. (#2294)
Browse files Browse the repository at this point in the history
* fix #2293: Properly encode Decimals without any decimal places.

* doc: Added changelog entry.

* refactor: Move ConstrainedDecimal test from separate file into test_json

* docs: Remove prefix from changelog.

* test: Changed test_con_decimal_encode to @samuelcolvins recommendations
  • Loading branch information
Hultner authored Feb 24, 2021
1 parent c8883e3 commit eab9d05
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 2 deletions.
1 change: 1 addition & 0 deletions changes/2293-hultner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Properly encode `Decimal` with, or without any decimal places.
23 changes: 22 additions & 1 deletion pydantic/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,35 @@ def isoformat(o: Union[datetime.date, datetime.time]) -> str:
return o.isoformat()


def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
"""
Encodes a Decimal as int of there's no exponent, otherwise float
This is useful when we use ConstrainedDecimal to represent Numeric(x,0)
where a integer (but not int typed) is used. Encoding this as a float
results in failed round-tripping between encode and prase.
Our Id type is a prime example of this.
>>> decimal_encoder(Decimal("1.0"))
1.0
>>> decimal_encoder(Decimal("1"))
1
"""
if dec_value.as_tuple().exponent >= 0:
return int(dec_value)
else:
return float(dec_value)


ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
bytes: lambda o: o.decode(),
Color: str,
datetime.date: isoformat,
datetime.datetime: isoformat,
datetime.time: isoformat,
datetime.timedelta: lambda td: td.total_seconds(),
Decimal: float,
Decimal: decimal_encoder,
Enum: lambda o: o.value,
frozenset: list,
deque: list,
Expand Down
21 changes: 20 additions & 1 deletion tests/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from pydantic.color import Color
from pydantic.dataclasses import dataclass as pydantic_dataclass
from pydantic.json import pydantic_encoder, timedelta_isoformat
from pydantic.types import DirectoryPath, FilePath, SecretBytes, SecretStr
from pydantic.types import ConstrainedDecimal, DirectoryPath, FilePath, SecretBytes, SecretStr


class MyEnum(Enum):
Expand Down Expand Up @@ -170,6 +170,25 @@ class Config:
assert m.json() == '{"x": "P0DT0H2M3.000000S"}'


def test_con_decimal_encode() -> None:
"""
Makes sure a decimal with decimal_places = 0, as well as one with places
can handle a encode/decode roundtrip.
"""

class Id(ConstrainedDecimal):
max_digits = 22
decimal_places = 0
ge = 0

class Obj(BaseModel):
id: Id
price: Decimal = Decimal('0.01')

assert Obj(id=1).json() == '{"id": 1, "price": 0.01}'
assert Obj.parse_raw('{"id": 1, "price": 0.01}') == Obj(id=1)


def test_json_encoder_simple_inheritance():
class Parent(BaseModel):
dt: datetime.datetime = datetime.datetime.now()
Expand Down

0 comments on commit eab9d05

Please sign in to comment.