Home Assistant Unofficial Reference 2024.12.1
json.py
Go to the documentation of this file.
1 """Helpers to help with encoding Home Assistant objects in JSON."""
2 
3 from collections import deque
4 from collections.abc import Callable
5 import datetime
6 from functools import partial
7 import json
8 import logging
9 from pathlib import Path
10 from typing import TYPE_CHECKING, Any, Final
11 
12 import orjson
13 
14 from homeassistant.util.file import write_utf8_file, write_utf8_file_atomic
15 from homeassistant.util.json import ( # noqa: F401
16  JSON_DECODE_EXCEPTIONS as _JSON_DECODE_EXCEPTIONS,
17  JSON_ENCODE_EXCEPTIONS as _JSON_ENCODE_EXCEPTIONS,
18  SerializationError,
19  format_unserializable_data,
20  json_loads as _json_loads,
21 )
22 
23 from .deprecation import (
24  DeprecatedConstant,
25  all_with_deprecated_constants,
26  check_if_deprecated_constant,
27  deprecated_function,
28  dir_with_deprecated_constants,
29 )
30 
31 _DEPRECATED_JSON_DECODE_EXCEPTIONS = DeprecatedConstant(
32  _JSON_DECODE_EXCEPTIONS, "homeassistant.util.json.JSON_DECODE_EXCEPTIONS", "2025.8"
33 )
34 _DEPRECATED_JSON_ENCODE_EXCEPTIONS = DeprecatedConstant(
35  _JSON_ENCODE_EXCEPTIONS, "homeassistant.util.json.JSON_ENCODE_EXCEPTIONS", "2025.8"
36 )
37 json_loads = deprecated_function(
38  "homeassistant.util.json.json_loads", breaks_in_ha_version="2025.8"
39 )(_json_loads)
40 
41 # These can be removed if no deprecated constant are in this module anymore
42 __getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
43 __dir__ = partial(
44  dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
45 )
46 __all__ = all_with_deprecated_constants(globals())
47 
48 
49 _LOGGER = logging.getLogger(__name__)
50 
51 
53  """JSONEncoder that supports Home Assistant objects."""
54 
55  def default(self, o: Any) -> Any:
56  """Convert Home Assistant objects.
57 
58  Hand other objects to the original method.
59  """
60  if isinstance(o, datetime.datetime):
61  return o.isoformat()
62  if isinstance(o, set):
63  return list(o)
64  if hasattr(o, "as_dict"):
65  return o.as_dict()
66 
67  return json.JSONEncoder.default(self, o)
68 
69 
70 def json_encoder_default(obj: Any) -> Any:
71  """Convert Home Assistant objects.
72 
73  Hand other objects to the original method.
74  """
75  if hasattr(obj, "json_fragment"):
76  return obj.json_fragment
77  if isinstance(obj, (set, tuple)):
78  return list(obj)
79  if isinstance(obj, float):
80  return float(obj)
81  if hasattr(obj, "as_dict"):
82  return obj.as_dict()
83  if isinstance(obj, Path):
84  return obj.as_posix()
85  if isinstance(obj, datetime.datetime):
86  return obj.isoformat()
87  raise TypeError
88 
89 
90 if TYPE_CHECKING:
91 
92  def json_bytes(obj: Any) -> bytes:
93  """Dump json bytes."""
94 
95 else:
96  json_bytes = partial(
97  orjson.dumps, option=orjson.OPT_NON_STR_KEYS, default=json_encoder_default
98  )
99  """Dump json bytes."""
100 
101 
103  """JSONEncoder that supports Home Assistant objects and falls back to repr(o)."""
104 
105  def default(self, o: Any) -> Any:
106  """Convert certain objects.
107 
108  Fall back to repr(o).
109  """
110  if isinstance(o, datetime.timedelta):
111  return {"__type": str(type(o)), "total_seconds": o.total_seconds()}
112  if isinstance(o, datetime.datetime):
113  return o.isoformat()
114  if isinstance(o, (datetime.date, datetime.time)):
115  return {"__type": str(type(o)), "isoformat": o.isoformat()}
116  try:
117  return super().default(o)
118  except TypeError:
119  return {"__type": str(type(o)), "repr": repr(o)}
120 
121 
122 def _strip_null(obj: Any) -> Any:
123  """Strip NUL from an object."""
124  if isinstance(obj, str):
125  return obj.split("\0", 1)[0]
126  if isinstance(obj, dict):
127  return {key: _strip_null(o) for key, o in obj.items()}
128  if isinstance(obj, list):
129  return [_strip_null(o) for o in obj]
130  return obj
131 
132 
133 def json_bytes_strip_null(data: Any) -> bytes:
134  """Dump json bytes after terminating strings at the first NUL."""
135  # We expect null-characters to be very rare, hence try encoding first and look
136  # for an escaped null-character in the output.
137  result = json_bytes(data)
138  if b"\\u0000" not in result:
139  return result
140 
141  # We work on the processed result so we don't need to worry about
142  # Home Assistant extensions which allows encoding sets, tuples, etc.
143  return json_bytes(_strip_null(orjson.loads(result)))
144 
145 
146 json_fragment = orjson.Fragment
147 
148 
149 def json_dumps(data: Any) -> str:
150  r"""Dump json string.
151 
152  orjson supports serializing dataclasses natively which
153  eliminates the need to implement as_dict in many places
154  when the data is already in a dataclass. This works
155  well as long as all the data in the dataclass can also
156  be serialized.
157 
158  If it turns out to be a problem we can disable this
159  with option \|= orjson.OPT_PASSTHROUGH_DATACLASS and it
160  will fallback to as_dict
161  """
162  return json_bytes(data).decode("utf-8")
163 
164 
165 json_bytes_sorted = partial(
166  orjson.dumps,
167  option=orjson.OPT_NON_STR_KEYS | orjson.OPT_SORT_KEYS,
168  default=json_encoder_default,
169 )
170 """Dump json bytes with keys sorted."""
171 
172 
173 def json_dumps_sorted(data: Any) -> str:
174  """Dump json string with keys sorted."""
175  return json_bytes_sorted(data).decode("utf-8")
176 
177 
178 JSON_DUMP: Final = json_dumps
179 
180 
181 def _orjson_default_encoder(data: Any) -> str:
182  """JSON encoder that uses orjson with hass defaults and returns a str."""
183  return _orjson_bytes_default_encoder(data).decode("utf-8")
184 
185 
186 def _orjson_bytes_default_encoder(data: Any) -> bytes:
187  """JSON encoder that uses orjson with hass defaults and returns bytes."""
188  return orjson.dumps(
189  data,
190  option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS,
191  default=json_encoder_default,
192  )
193 
194 
196  filename: str,
197  data: list | dict,
198  private: bool = False,
199  *,
200  encoder: type[json.JSONEncoder] | None = None,
201  atomic_writes: bool = False,
202 ) -> None:
203  """Save JSON data to a file."""
204  dump: Callable[[Any], Any]
205  try:
206  # For backwards compatibility, if they pass in the
207  # default json encoder we use _orjson_default_encoder
208  # which is the orjson equivalent to the default encoder.
209  if encoder and encoder is not JSONEncoder:
210  # If they pass a custom encoder that is not the
211  # default JSONEncoder, we use the slow path of json.dumps
212  mode = "w"
213  dump = json.dumps
214  json_data: str | bytes = json.dumps(data, indent=2, cls=encoder)
215  else:
216  mode = "wb"
217  dump = _orjson_default_encoder
218  json_data = _orjson_bytes_default_encoder(data)
219  except TypeError as error:
220  formatted_data = format_unserializable_data(
221  find_paths_unserializable_data(data, dump=dump)
222  )
223  msg = f"Failed to serialize to JSON: {filename}. Bad data at {formatted_data}"
224  _LOGGER.error(msg)
225  raise SerializationError(msg) from error
226 
227  method = write_utf8_file_atomic if atomic_writes else write_utf8_file
228  method(filename, json_data, private, mode=mode)
229 
230 
232  bad_data: Any, *, dump: Callable[[Any], str] = json.dumps
233 ) -> dict[str, Any]:
234  """Find the paths to unserializable data.
235 
236  This method is slow! Only use for error handling.
237  """
238  from homeassistant.core import ( # pylint: disable=import-outside-toplevel
239  Event,
240  State,
241  )
242 
243  to_process = deque([(bad_data, "$")])
244  invalid = {}
245 
246  while to_process:
247  obj, obj_path = to_process.popleft()
248 
249  try:
250  dump(obj)
251  continue
252  except (ValueError, TypeError):
253  pass
254 
255  # We convert objects with as_dict to their dict values
256  # so we can find bad data inside it
257  if hasattr(obj, "as_dict"):
258  desc = obj.__class__.__name__
259  if isinstance(obj, State):
260  desc += f": {obj.entity_id}"
261  elif isinstance(obj, Event):
262  desc += f": {obj.event_type}"
263 
264  obj_path += f"({desc})"
265  obj = obj.as_dict()
266 
267  if isinstance(obj, dict):
268  for key, value in obj.items():
269  try:
270  # Is key valid?
271  dump({key: None})
272  except TypeError:
273  invalid[f"{obj_path}<key: {key}>"] = key
274  else:
275  # Process value
276  to_process.append((value, f"{obj_path}.{key}"))
277  elif isinstance(obj, list):
278  for idx, value in enumerate(obj):
279  to_process.append((value, f"{obj_path}[{idx}]"))
280  else:
281  invalid[obj_path] = obj
282 
283  return invalid
list[str] all_with_deprecated_constants(dict[str, Any] module_globals)
Definition: deprecation.py:356
bytes _orjson_bytes_default_encoder(Any data)
Definition: json.py:186
Any _strip_null(Any obj)
Definition: json.py:122
str json_dumps(Any data)
Definition: json.py:149
str _orjson_default_encoder(Any data)
Definition: json.py:181
bytes json_bytes_strip_null(Any data)
Definition: json.py:133
str json_dumps_sorted(Any data)
Definition: json.py:173
Any json_encoder_default(Any obj)
Definition: json.py:70
dict[str, Any] find_paths_unserializable_data(Any bad_data, *Callable[[Any], str] dump=json.dumps)
Definition: json.py:233
None save_json(str filename, list|dict data, bool private=False, *type[json.JSONEncoder]|None encoder=None, bool atomic_writes=False)
Definition: json.py:202
str format_unserializable_data(dict[str, Any] data)
Definition: json.py:126
str dump(dict|list _dict)
Definition: dumper.py:21