json.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. import datetime
  2. import warnings
  3. from collections import deque
  4. from decimal import Decimal
  5. from enum import Enum
  6. from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network
  7. from pathlib import Path
  8. from re import Pattern
  9. from types import GeneratorType
  10. from typing import TYPE_CHECKING, Any, Callable, Dict, Type, Union
  11. from uuid import UUID
  12. from typing_extensions import deprecated
  13. from ..color import Color
  14. from ..networks import NameEmail
  15. from ..types import SecretBytes, SecretStr
  16. from ..warnings import PydanticDeprecatedSince20
  17. if not TYPE_CHECKING:
  18. # See PyCharm issues https://youtrack.jetbrains.com/issue/PY-21915
  19. # and https://youtrack.jetbrains.com/issue/PY-51428
  20. DeprecationWarning = PydanticDeprecatedSince20
  21. __all__ = 'pydantic_encoder', 'custom_pydantic_encoder', 'timedelta_isoformat'
  22. def isoformat(o: Union[datetime.date, datetime.time]) -> str:
  23. return o.isoformat()
  24. def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
  25. """Encodes a Decimal as int of there's no exponent, otherwise float.
  26. This is useful when we use ConstrainedDecimal to represent Numeric(x,0)
  27. where a integer (but not int typed) is used. Encoding this as a float
  28. results in failed round-tripping between encode and parse.
  29. Our Id type is a prime example of this.
  30. >>> decimal_encoder(Decimal("1.0"))
  31. 1.0
  32. >>> decimal_encoder(Decimal("1"))
  33. 1
  34. """
  35. exponent = dec_value.as_tuple().exponent
  36. if isinstance(exponent, int) and exponent >= 0:
  37. return int(dec_value)
  38. else:
  39. return float(dec_value)
  40. ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
  41. bytes: lambda o: o.decode(),
  42. Color: str,
  43. datetime.date: isoformat,
  44. datetime.datetime: isoformat,
  45. datetime.time: isoformat,
  46. datetime.timedelta: lambda td: td.total_seconds(),
  47. Decimal: decimal_encoder,
  48. Enum: lambda o: o.value,
  49. frozenset: list,
  50. deque: list,
  51. GeneratorType: list,
  52. IPv4Address: str,
  53. IPv4Interface: str,
  54. IPv4Network: str,
  55. IPv6Address: str,
  56. IPv6Interface: str,
  57. IPv6Network: str,
  58. NameEmail: str,
  59. Path: str,
  60. Pattern: lambda o: o.pattern,
  61. SecretBytes: str,
  62. SecretStr: str,
  63. set: list,
  64. UUID: str,
  65. }
  66. @deprecated(
  67. 'pydantic_encoder is deprecated, use pydantic_core.to_jsonable_python instead.', category=PydanticDeprecatedSince20
  68. )
  69. def pydantic_encoder(obj: Any) -> Any:
  70. from dataclasses import asdict, is_dataclass
  71. from ..main import BaseModel
  72. warnings.warn('pydantic_encoder is deprecated, use BaseModel.model_dump instead.', DeprecationWarning, stacklevel=2)
  73. if isinstance(obj, BaseModel):
  74. return obj.model_dump()
  75. elif is_dataclass(obj):
  76. return asdict(obj)
  77. # Check the class type and its superclasses for a matching encoder
  78. for base in obj.__class__.__mro__[:-1]:
  79. try:
  80. encoder = ENCODERS_BY_TYPE[base]
  81. except KeyError:
  82. continue
  83. return encoder(obj)
  84. else: # We have exited the for loop without finding a suitable encoder
  85. raise TypeError(f"Object of type '{obj.__class__.__name__}' is not JSON serializable")
  86. # TODO: Add a suggested migration path once there is a way to use custom encoders
  87. @deprecated('custom_pydantic_encoder is deprecated.', category=PydanticDeprecatedSince20)
  88. def custom_pydantic_encoder(type_encoders: Dict[Any, Callable[[Type[Any]], Any]], obj: Any) -> Any:
  89. # Check the class type and its superclasses for a matching encoder
  90. warnings.warn(
  91. 'custom_pydantic_encoder is deprecated, use BaseModel.model_dump instead.', DeprecationWarning, stacklevel=2
  92. )
  93. for base in obj.__class__.__mro__[:-1]:
  94. try:
  95. encoder = type_encoders[base]
  96. except KeyError:
  97. continue
  98. return encoder(obj)
  99. else: # We have exited the for loop without finding a suitable encoder
  100. return pydantic_encoder(obj)
  101. @deprecated('timedelta_isoformat is deprecated.', category=PydanticDeprecatedSince20)
  102. def timedelta_isoformat(td: datetime.timedelta) -> str:
  103. """ISO 8601 encoding for Python timedelta object."""
  104. warnings.warn('timedelta_isoformat is deprecated.', DeprecationWarning, stacklevel=2)
  105. minutes, seconds = divmod(td.seconds, 60)
  106. hours, minutes = divmod(minutes, 60)
  107. return f'{"-" if td.days < 0 else ""}P{abs(td.days)}DT{hours:d}H{minutes:d}M{seconds:d}.{td.microseconds:06d}S'