1# ---------------------------------------------------------------------
2# JSON normalization routines
3from __future__ import annotations
4
5from collections import (
6 abc,
7 defaultdict,
8)
9import copy
10from typing import (
11 TYPE_CHECKING,
12 Any,
13 DefaultDict,
14 overload,
15)
16
17import numpy as np
18
19from pandas._libs.writers import convert_json_to_lines
20from pandas.util._decorators import set_module
21
22from pandas.core.dtypes.common import is_scalar
23
24import pandas as pd
25from pandas import (
26 DataFrame,
27 Series,
28)
29
30if TYPE_CHECKING:
31 from collections.abc import Iterable
32
33 from pandas._typing import (
34 IgnoreRaise,
35 Scalar,
36 )
37
38
39def convert_to_line_delimits(s: str) -> str:
40 """
41 Helper function that converts JSON lists to line delimited JSON.
42 """
43 # Determine we have a JSON list to turn to lines otherwise just return the
44 # json object, only lists can
45 if not s[0] == "[" and s[-1] == "]":
46 return s
47 s = s[1:-1]
48
49 return convert_json_to_lines(s)
50
51
52@overload
53def nested_to_record(
54 ds: dict,
55 prefix: str = ...,
56 sep: str = ...,
57 level: int = ...,
58 max_level: int | None = ...,
59) -> dict[str, Any]: ...
60
61
62@overload
63def nested_to_record(
64 ds: list[dict],
65 prefix: str = ...,
66 sep: str = ...,
67 level: int = ...,
68 max_level: int | None = ...,
69) -> list[dict[str, Any]]: ...
70
71
72def nested_to_record(
73 ds: dict | list[dict],
74 prefix: str = "",
75 sep: str = ".",
76 level: int = 0,
77 max_level: int | None = None,
78) -> dict[str, Any] | list[dict[str, Any]]:
79 """
80 A simplified json_normalize
81
82 Converts a nested dict into a flat dict ("record"), unlike json_normalize,
83 it does not attempt to extract a subset of the data.
84
85 Parameters
86 ----------
87 ds : dict or list of dicts
88 prefix: the prefix, optional, default: ""
89 sep : str, default '.'
90 Nested records will generate names separated by sep,
91 e.g., for sep='.', { 'foo' : { 'bar' : 0 } } -> foo.bar
92 level: int, optional, default: 0
93 The number of levels in the json string.
94
95 max_level: int, optional, default: None
96 The max depth to normalize.
97
98 Returns
99 -------
100 d - dict or list of dicts, matching `ds`
101
102 Examples
103 --------
104 >>> nested_to_record(
105 ... dict(flat1=1, dict1=dict(c=1, d=2), nested=dict(e=dict(c=1, d=2), d=2))
106 ... )
107 {\
108'flat1': 1, \
109'dict1.c': 1, \
110'dict1.d': 2, \
111'nested.e.c': 1, \
112'nested.e.d': 2, \
113'nested.d': 2\
114}
115 """
116 singleton = False
117 if isinstance(ds, dict):
118 ds = [ds]
119 singleton = True
120 new_ds = []
121 for d in ds:
122 new_d = copy.deepcopy(d)
123 for k, v in d.items():
124 # each key gets renamed with prefix
125 if not isinstance(k, str):
126 k = str(k)
127 if level == 0:
128 newkey = k
129 else:
130 newkey = prefix + sep + k
131
132 # flatten if type is dict and
133 # current dict level < maximum level provided and
134 # only dicts gets recurse-flattened
135 # only at level>1 do we rename the rest of the keys
136 if not isinstance(v, dict) or (
137 max_level is not None and level >= max_level
138 ):
139 if level != 0: # so we skip copying for top level, common case
140 v = new_d.pop(k)
141 new_d[newkey] = v
142 continue
143
144 v = new_d.pop(k)
145 new_d.update(nested_to_record(v, newkey, sep, level + 1, max_level))
146 new_ds.append(new_d)
147
148 if singleton:
149 return new_ds[0]
150 return new_ds
151
152
153def _normalize_json(
154 data: Any,
155 key_string: str,
156 normalized_dict: dict[str, Any],
157 separator: str,
158) -> dict[str, Any]:
159 """
160 Main recursive function
161 Designed for the most basic use case of pd.json_normalize(data)
162 intended as a performance improvement, see #15621
163
164 Parameters
165 ----------
166 data : Any
167 Type dependent on types contained within nested Json
168 key_string : str
169 New key (with separator(s) in) for data
170 normalized_dict : dict
171 The new normalized/flattened Json dict
172 separator : str, default '.'
173 Nested records will generate names separated by sep,
174 e.g., for sep='.', { 'foo' : { 'bar' : 0 } } -> foo.bar
175 """
176 if isinstance(data, dict):
177 for key, value in data.items():
178 new_key = f"{key_string}{separator}{key}"
179
180 if not key_string:
181 new_key = new_key.removeprefix(separator)
182
183 _normalize_json(
184 data=value,
185 key_string=new_key,
186 normalized_dict=normalized_dict,
187 separator=separator,
188 )
189 else:
190 normalized_dict[key_string] = data
191 return normalized_dict
192
193
194def _normalize_json_ordered(data: dict[str, Any], separator: str) -> dict[str, Any]:
195 """
196 Order the top level keys and then recursively go to depth
197
198 Parameters
199 ----------
200 data : dict or list of dicts
201 separator : str, default '.'
202 Nested records will generate names separated by sep,
203 e.g., for sep='.', { 'foo' : { 'bar' : 0 } } -> foo.bar
204
205 Returns
206 -------
207 dict or list of dicts, matching `normalized_json_object`
208 """
209 top_dict_ = {k: v for k, v in data.items() if not isinstance(v, dict)}
210 nested_dict_ = _normalize_json(
211 data={k: v for k, v in data.items() if isinstance(v, dict)},
212 key_string="",
213 normalized_dict={},
214 separator=separator,
215 )
216 return {**top_dict_, **nested_dict_}
217
218
219def _simple_json_normalize(
220 ds: dict | list[dict],
221 sep: str = ".",
222) -> dict | list[dict] | Any:
223 """
224 An optimized basic json_normalize
225
226 Converts a nested dict into a flat dict ("record"), unlike
227 json_normalize and nested_to_record it doesn't do anything clever.
228 But for the most basic use cases it enhances performance.
229 E.g. pd.json_normalize(data)
230
231 Parameters
232 ----------
233 ds : dict or list of dicts
234 sep : str, default '.'
235 Nested records will generate names separated by sep,
236 e.g., for sep='.', { 'foo' : { 'bar' : 0 } } -> foo.bar
237
238 Returns
239 -------
240 frame : DataFrame
241 d - dict or list of dicts, matching `normalized_json_object`
242
243 Examples
244 --------
245 >>> _simple_json_normalize(
246 ... {
247 ... "flat1": 1,
248 ... "dict1": {"c": 1, "d": 2},
249 ... "nested": {"e": {"c": 1, "d": 2}, "d": 2},
250 ... }
251 ... )
252 {\
253'flat1': 1, \
254'dict1.c': 1, \
255'dict1.d': 2, \
256'nested.e.c': 1, \
257'nested.e.d': 2, \
258'nested.d': 2\
259}
260
261 """
262 normalized_json_object = {}
263 # expect a dictionary, as most jsons are. However, lists are perfectly valid
264 if isinstance(ds, dict):
265 normalized_json_object = _normalize_json_ordered(data=ds, separator=sep)
266 elif isinstance(ds, list):
267 normalized_json_list = [_simple_json_normalize(row, sep=sep) for row in ds]
268 return normalized_json_list
269 return normalized_json_object
270
271
272def _validate_meta(meta: str | list[str | list[str]] | None) -> None:
273 """
274 Validate that meta parameter contains only strings or lists of strings.
275 Parameters
276 ----------
277 meta : str or list of str or list of list of str or None
278 The meta parameter to validate.
279 Raises
280 ------
281 TypeError
282 If meta contains elements that are not strings or lists of strings.
283 """
284 if meta is None:
285 return
286 if isinstance(meta, str):
287 return
288 for item in meta:
289 if isinstance(item, list):
290 for subitem in item:
291 if not isinstance(subitem, str):
292 raise TypeError(
293 "All elements in nested meta paths must be strings. "
294 f"Found {type(subitem).__name__}: {subitem!r}"
295 )
296 elif not isinstance(item, str):
297 raise TypeError(
298 "All elements in 'meta' must be strings or lists of strings. "
299 f"Found {type(item).__name__}: {item!r}"
300 )
301
302
303@set_module("pandas")
304def json_normalize(
305 data: dict | list[dict] | Series,
306 record_path: str | list | None = None,
307 meta: str | list[str | list[str]] | None = None,
308 meta_prefix: str | None = None,
309 record_prefix: str | None = None,
310 errors: IgnoreRaise = "raise",
311 sep: str = ".",
312 max_level: int | None = None,
313) -> DataFrame:
314 """
315 Normalize semi-structured JSON data into a flat table.
316
317 This method is designed to transform semi-structured JSON data, such as nested
318 dictionaries or lists, into a flat table. This is particularly useful when
319 handling JSON-like data structures that contain deeply nested fields.
320
321 Parameters
322 ----------
323 data : dict, list of dicts, or Series of dicts
324 Unserialized JSON objects.
325 record_path : str or list of str, default None
326 Path in each object to list of records. If not passed, data will be
327 assumed to be an array of records.
328 meta : list of paths (str or list of str), default None
329 Fields to use as metadata for each record in resulting table.
330 meta_prefix : str, default None
331 String to prefix records with dotted path, e.g. foo.bar.field if
332 meta is ['foo', 'bar'].
333 record_prefix : str, default None
334 String to prefix records with dotted path, e.g. foo.bar.field if
335 path to records is ['foo', 'bar'].
336 errors : {'raise', 'ignore'}, default 'raise'
337 Configures error handling.
338
339 * 'ignore' : will ignore KeyError if keys listed in meta are not
340 always present.
341 * 'raise' : will raise KeyError if keys listed in meta are not
342 always present.
343 sep : str, default '.'
344 Nested records will generate names separated by sep.
345 e.g., for sep='.', {'foo': {'bar': 0}} -> foo.bar.
346 max_level : int, default None
347 Max number of levels(depth of dict) to normalize.
348 if None, normalizes all levels.
349
350 Returns
351 -------
352 DataFrame
353 The normalized data, represented as a pandas DataFrame.
354
355 See Also
356 --------
357 DataFrame : Two-dimensional, size-mutable, potentially heterogeneous tabular data.
358 Series : One-dimensional ndarray with axis labels (including time series).
359
360 Examples
361 --------
362 >>> data = [
363 ... {"id": 1, "name": {"first": "Coleen", "last": "Volk"}},
364 ... {"name": {"given": "Mark", "family": "Regner"}},
365 ... {"id": 2, "name": "Faye Raker"},
366 ... ]
367 >>> pd.json_normalize(data)
368 id name.first name.last name.given name.family name
369 0 1.0 Coleen Volk NaN NaN NaN
370 1 NaN NaN NaN Mark Regner NaN
371 2 2.0 NaN NaN NaN NaN Faye Raker
372
373 >>> data = [
374 ... {
375 ... "id": 1,
376 ... "name": "Cole Volk",
377 ... "fitness": {"height": 130, "weight": 60},
378 ... },
379 ... {"name": "Mark Reg", "fitness": {"height": 130, "weight": 60}},
380 ... {
381 ... "id": 2,
382 ... "name": "Faye Raker",
383 ... "fitness": {"height": 130, "weight": 60},
384 ... },
385 ... ]
386 >>> pd.json_normalize(data, max_level=0)
387 id name fitness
388 0 1.0 Cole Volk {'height': 130, 'weight': 60}
389 1 NaN Mark Reg {'height': 130, 'weight': 60}
390 2 2.0 Faye Raker {'height': 130, 'weight': 60}
391
392 Normalizes nested data up to level 1.
393
394 >>> data = [
395 ... {
396 ... "id": 1,
397 ... "name": "Cole Volk",
398 ... "fitness": {"height": 130, "weight": 60},
399 ... },
400 ... {"name": "Mark Reg", "fitness": {"height": 130, "weight": 60}},
401 ... {
402 ... "id": 2,
403 ... "name": "Faye Raker",
404 ... "fitness": {"height": 130, "weight": 60},
405 ... },
406 ... ]
407 >>> pd.json_normalize(data, max_level=1)
408 id name fitness.height fitness.weight
409 0 1.0 Cole Volk 130 60
410 1 NaN Mark Reg 130 60
411 2 2.0 Faye Raker 130 60
412
413 >>> data = [
414 ... {
415 ... "id": 1,
416 ... "name": "Cole Volk",
417 ... "fitness": {"height": 130, "weight": 60},
418 ... },
419 ... {"name": "Mark Reg", "fitness": {"height": 130, "weight": 60}},
420 ... {
421 ... "id": 2,
422 ... "name": "Faye Raker",
423 ... "fitness": {"height": 130, "weight": 60},
424 ... },
425 ... ]
426 >>> series = pd.Series(data, index=pd.Index(["a", "b", "c"]))
427 >>> pd.json_normalize(series)
428 id name fitness.height fitness.weight
429 a 1.0 Cole Volk 130 60
430 b NaN Mark Reg 130 60
431 c 2.0 Faye Raker 130 60
432
433 >>> data = [
434 ... {
435 ... "state": "Florida",
436 ... "shortname": "FL",
437 ... "info": {"governor": "Rick Scott"},
438 ... "counties": [
439 ... {"name": "Dade", "population": 12345},
440 ... {"name": "Broward", "population": 40000},
441 ... {"name": "Palm Beach", "population": 60000},
442 ... ],
443 ... },
444 ... {
445 ... "state": "Ohio",
446 ... "shortname": "OH",
447 ... "info": {"governor": "John Kasich"},
448 ... "counties": [
449 ... {"name": "Summit", "population": 1234},
450 ... {"name": "Cuyahoga", "population": 1337},
451 ... ],
452 ... },
453 ... ]
454 >>> result = pd.json_normalize(
455 ... data, "counties", ["state", "shortname", ["info", "governor"]]
456 ... )
457 >>> result
458 name population state shortname info.governor
459 0 Dade 12345 Florida FL Rick Scott
460 1 Broward 40000 Florida FL Rick Scott
461 2 Palm Beach 60000 Florida FL Rick Scott
462 3 Summit 1234 Ohio OH John Kasich
463 4 Cuyahoga 1337 Ohio OH John Kasich
464
465 >>> data = {"A": [1, 2]}
466 >>> pd.json_normalize(data, "A", record_prefix="Prefix.")
467 Prefix.0
468 0 1
469 1 2
470
471 Returns normalized data with columns prefixed with the given string.
472 """
473 _validate_meta(meta)
474
475 def _pull_field(
476 js: dict[str, Any], spec: list | str, extract_record: bool = False
477 ) -> Scalar | Iterable:
478 """Internal function to pull field"""
479 result = js
480 try:
481 if isinstance(spec, list):
482 for field in spec:
483 if result is None:
484 raise KeyError(field)
485 result = result[field]
486 else:
487 result = result[spec]
488 except KeyError as e:
489 if extract_record:
490 raise KeyError(
491 f"Key {e} not found. If specifying a record_path, all elements of "
492 f"data should have the path."
493 ) from e
494 if errors == "ignore":
495 return np.nan
496 else:
497 raise KeyError(
498 f"Key {e} not found. To replace missing values of {e} with "
499 f"np.nan, pass in errors='ignore'"
500 ) from e
501
502 return result
503
504 def _pull_records(js: dict[str, Any], spec: list | str) -> list:
505 """
506 Internal function to pull field for records, and similar to
507 _pull_field, but require to return list. And will raise error
508 if has non iterable value.
509 """
510 result = _pull_field(js, spec, extract_record=True)
511
512 # GH 31507 GH 30145, GH 26284 if result is not list, raise TypeError if not
513 # null, otherwise return an empty list
514 if not isinstance(result, list):
515 if pd.isnull(result):
516 result = []
517 else:
518 raise TypeError(
519 f"Path must contain list or null, "
520 f"but got {type(result).__name__} at {spec!r}"
521 )
522 return result
523
524 if isinstance(data, Series):
525 index = data.index
526 else:
527 index = None
528
529 if isinstance(data, list) and not data:
530 return DataFrame()
531 elif isinstance(data, dict):
532 # A bit of a hackjob
533 data = [data]
534 elif isinstance(data, abc.Iterable) and not isinstance(data, str):
535 # GH35923 Fix pd.json_normalize to not skip the first element of a
536 # generator input
537 data = list(data)
538 for i, item in enumerate(data):
539 if isinstance(item, dict):
540 continue
541 if is_scalar(item) and pd.isna(item):
542 data[i] = {}
543 else:
544 msg = (
545 "All items in data must be of type dict or NA-like, "
546 f"found {type(item).__name__}"
547 )
548 raise TypeError(msg)
549 else:
550 raise NotImplementedError
551
552 # check to see if a simple recursive function is possible to
553 # improve performance (see #15621) but only for cases such
554 # as pd.Dataframe(data) or pd.Dataframe(data, sep)
555 if (
556 record_path is None
557 and meta is None
558 and meta_prefix is None
559 and record_prefix is None
560 and max_level is None
561 ):
562 return DataFrame(_simple_json_normalize(data, sep=sep), index=index)
563
564 if record_path is None:
565 if any([isinstance(x, dict) for x in y.values()] for y in data):
566 # naive normalization, this is idempotent for flat records
567 # and potentially will inflate the data considerably for
568 # deeply nested structures:
569 # {VeryLong: { b: 1,c:2}} -> {VeryLong.b:1 ,VeryLong.c:@}
570 #
571 # TODO: handle record value which are lists, at least error
572 # reasonably
573 data = nested_to_record(data, sep=sep, max_level=max_level)
574 result = DataFrame(data, index=index)
575 if record_prefix is not None:
576 result = result.rename(columns=lambda x: f"{record_prefix}{x}")
577 return result
578 elif not isinstance(record_path, list):
579 record_path = [record_path]
580
581 if meta is None:
582 meta = []
583 elif not isinstance(meta, list):
584 meta = [meta]
585
586 _meta = [m if isinstance(m, list) else [m] for m in meta]
587
588 # Disastrously inefficient for now
589 records: list = []
590 lengths = []
591
592 meta_vals: DefaultDict = defaultdict(list)
593 meta_keys = [sep.join(val) for val in _meta]
594
595 def _recursive_extract(data, path, seen_meta, level: int = 0) -> None:
596 if isinstance(data, dict):
597 data = [data]
598 if len(path) > 1:
599 for obj in data:
600 for val, key in zip(_meta, meta_keys, strict=True):
601 if level + 1 == len(val):
602 seen_meta[key] = _pull_field(obj, val[-1])
603
604 _recursive_extract(obj[path[0]], path[1:], seen_meta, level=level + 1)
605 else:
606 for obj in data:
607 recs = _pull_records(obj, path[0])
608 recs = [
609 nested_to_record(r, sep=sep, max_level=max_level)
610 if isinstance(r, dict)
611 else r
612 for r in recs
613 ]
614
615 # For repeating the metadata later
616 lengths.append(len(recs))
617 for val, key in zip(_meta, meta_keys, strict=True):
618 if level + 1 > len(val):
619 meta_val = seen_meta[key]
620 else:
621 meta_val = _pull_field(obj, val[level:])
622 meta_vals[key].append(meta_val)
623 records.extend(recs)
624
625 _recursive_extract(data, record_path, {}, level=0)
626
627 result = DataFrame(records)
628
629 if record_prefix is not None:
630 result = result.rename(columns=lambda x: f"{record_prefix}{x}")
631
632 # Data types, a problem
633 for k, v in meta_vals.items():
634 if meta_prefix is not None:
635 k = meta_prefix + k
636
637 if k in result:
638 raise ValueError(
639 f"Conflicting metadata name {k}, need distinguishing prefix "
640 )
641 # GH 37782
642
643 values = np.array(v, dtype=object)
644
645 if values.ndim > 1:
646 # GH 37782
647 values = np.empty((len(v),), dtype=object)
648 for i, val in enumerate(v):
649 values[i] = val
650
651 result[k] = values.repeat(lengths)
652 if index is not None:
653 result.index = index.repeat(lengths)
654 return result