Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix #2293: Properly encode Decimals without any decimal places. (#2294) #95

Merged
merged 1 commit into from
Feb 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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