Source code for bac_py.serialization.json
"""JSON serializer backed by orjson."""
from __future__ import annotations
import logging
from enum import IntEnum
from typing import Any
try:
import orjson
except ImportError: # pragma: no cover
orjson = None # type: ignore[assignment]
logger = logging.getLogger(__name__)
[docs]
def json_default(obj: object) -> object:
"""Default handler for serializing BACnet types to JSON.
Use as the *default* argument to :func:`json.dumps` or
:func:`orjson.dumps` so that BACnet objects serialize automatically.
Handles:
* Objects with a ``to_dict()`` method (``BitString``, ``ObjectIdentifier``,
``BACnetDate``, ``BACnetTime``, ``StatusFlags``, and all other BACnet
constructed types).
* ``bytes`` and ``memoryview`` → hex string.
* ``IntEnum`` subclasses (``ObjectType``, ``PropertyIdentifier``, …) → ``int``.
Example::
import json
from bac_py import json_default
result = await client.read_multiple(...)
print(json.dumps(result, default=json_default))
:param obj: The object to convert.
:returns: A JSON-serializable representation.
:raises TypeError: If *obj* is not a recognised type.
"""
if hasattr(obj, "to_dict"):
return obj.to_dict()
if isinstance(obj, bytes):
return obj.hex()
if isinstance(obj, memoryview):
return bytes(obj).hex()
if isinstance(obj, IntEnum):
return int(obj)
msg = f"Cannot serialize {type(obj).__name__}"
logger.warning("serialize failed: %s", msg)
raise TypeError(msg)
[docs]
class JsonSerializer:
"""JSON serializer using orjson for high-performance encoding.
``bytes`` and ``memoryview`` values are encoded as hex strings.
Since JSON has no binary type, deserialization returns them as
plain strings — callers must use ``bytes.fromhex()`` when
round-tripping binary data.
:param pretty: Indent output with 2 spaces.
:param sort_keys: Sort dict keys alphabetically.
"""
def __init__(
self,
*,
pretty: bool = False,
sort_keys: bool = False,
) -> None:
if orjson is None: # pragma: no cover
msg = "orjson is required for JsonSerializer — install bac-py[serialization]"
raise ImportError(msg)
self._options = orjson.OPT_NON_STR_KEYS
if pretty:
self._options |= orjson.OPT_INDENT_2
if sort_keys:
self._options |= orjson.OPT_SORT_KEYS
[docs]
def encode(self, data: dict[str, Any]) -> bytes:
"""Encode a dict to JSON bytes."""
return orjson.dumps(data, default=self._default, option=self._options)
[docs]
def decode(self, raw: bytes) -> dict[str, Any]:
"""Decode JSON bytes to a dict."""
result = orjson.loads(raw)
if not isinstance(result, dict):
msg = f"Expected JSON object, got {type(result).__name__}"
logger.warning("deserialize failed: %s", msg)
raise TypeError(msg)
return result
@property
def content_type(self) -> str:
"""MIME content type for JSON."""
return "application/json"
def _default(self, obj: Any) -> Any:
"""Handle BACnet types that orjson cannot serialize natively."""
return json_default(obj)