from dataclasses import is_dataclass, asdict
from enum import Enum
from json import JSONEncoder
from pathlib import Path
from typing import Any, Callable
from uuid import UUID
__all__ = ["CustomJSONEncoder"]
[docs]
class CustomJSONEncoder(JSONEncoder):
"""
Custom JSON encoder that extends Python's built-in JSONEncoder to handle additional Python types
and custom serialization logic.
Supported types and custom behaviors:
- Objects with a `to_json()` method: Serialized using that method.
- Datetime-like objects with `isoformat()`: Serialized to ISO 8601 format.
- `Path` objects: Serialized as POSIX-style strings.
- `bytes` and `bytearray`: Encoded as hexadecimal strings.
- `Enum` members: Serialized using their `.value`.
- `UUID` instances: Serialized as hexadecimal strings.
- `Exception` instances: Serialized as a dictionary with type, message, and args.
- Dataclass instances: Converted to dictionaries via `asdict`.
Args:
skipkeys (bool): Whether to skip keys not of a basic type (str, int, float, bool, None).
ensure_ascii (bool): If True, escapes all non-ASCII characters in output.
check_circular (bool): If True, checks for circular references and raises an error if found.
allow_nan (bool): If True, allows NaN and Infinity values (JavaScript-compatible).
sort_keys (bool): If True, output dictionary keys are sorted.
indent (int or None): If specified, pretty-prints output with given number of spaces.
separators (tuple or None): Tuple of (item_separator, key_separator) for customizing output.
default (callable or None): A fallback function for unsupported objects.
context (dict or None): Optional context dictionary for passing extra encoding parameters.
"""
def __init__(self, *, skipkeys: bool = False, ensure_ascii: bool = True, check_circular: bool = True,
allow_nan: bool = True, sort_keys: bool = False, indent: int | str | None = None,
separators: tuple[str, str] | None = None, default: Callable[[Any], Any] | None = None,
context: dict[str, Any] | None = None) -> None:
super().__init__(
skipkeys=skipkeys,
ensure_ascii=ensure_ascii,
check_circular=check_circular,
allow_nan=allow_nan,
sort_keys=sort_keys,
indent=indent,
separators=separators,
default=default,
)
self.context = context
[docs]
def default(self, data: Any) -> Any:
"""
Serializes unsupported data types into JSON-compatible formats.
Custom serialization behavior:
- If object has `to_json()` method: Calls and returns its result.
- If object has `isoformat()` method (e.g., datetime): Uses its ISO 8601 representation.
- If object is a `Path`: Returns its POSIX-style path string.
- If object is `bytes` or `bytearray`: Returns a hex-encoded string.
- If object is an `Enum`: Returns its `.value`.
- If object is a `UUID`: Returns its hexadecimal string.
- If object is an `Exception`: Returns a dict with type, message, and args.
- If object is a dataclass: Converts to a dictionary via `asdict()`.
Args:
data (Any): The object to be serialized.
Returns:
Any: A JSON-serializable object.
"""
if hasattr(data, "to_json"):
return data.to_json()
elif hasattr(data, "isoformat"):
return data.isoformat()
elif isinstance(data, Path):
return data.as_posix()
elif isinstance(data, (bytes, bytearray)):
return data.hex()
elif isinstance(data, Enum):
return data.value
elif isinstance(data, UUID):
return str(data)
elif isinstance(data, Exception):
return {
'type': type(data).__name__,
'message': str(data),
'args': data.args
}
elif is_dataclass(data) and not isinstance(data, type):
return asdict(data)
else:
return super().default(data)