From af463df03780ef6cf7930ec38d5a46452def2e08 Mon Sep 17 00:00:00 2001 From: dongwonmoon Date: Sat, 8 Nov 2025 00:23:33 +0900 Subject: [PATCH 1/4] BUG: Fix TypeError in json_normalize with non-str meta key and record_path --- pandas/io/json/_normalize.py | 15 ++++++++++---- pandas/tests/io/json/test_normalize.py | 28 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/pandas/io/json/_normalize.py b/pandas/io/json/_normalize.py index 6194e699c12a8..627d19de84f72 100644 --- a/pandas/io/json/_normalize.py +++ b/pandas/io/json/_normalize.py @@ -552,7 +552,12 @@ def _pull_records(js: dict[str, Any], spec: list | str) -> list: lengths = [] meta_vals: DefaultDict = defaultdict(list) - meta_keys = [sep.join(val) for val in _meta] + meta_keys = [] + for val in _meta: + if len(val) == 1: + meta_keys.append(val[0]) + else: + meta_keys.append(sep.join(str(x) for x in val)) def _recursive_extract(data, path, seen_meta, level: int = 0) -> None: if isinstance(data, dict): @@ -568,9 +573,11 @@ def _recursive_extract(data, path, seen_meta, level: int = 0) -> None: for obj in data: recs = _pull_records(obj, path[0]) recs = [ - nested_to_record(r, sep=sep, max_level=max_level) - if isinstance(r, dict) - else r + ( + nested_to_record(r, sep=sep, max_level=max_level) + if isinstance(r, dict) + else r + ) for r in recs ] diff --git a/pandas/tests/io/json/test_normalize.py b/pandas/tests/io/json/test_normalize.py index f03fd235fef85..07a4ff1ec472a 100644 --- a/pandas/tests/io/json/test_normalize.py +++ b/pandas/tests/io/json/test_normalize.py @@ -593,6 +593,34 @@ def test_series_index(self, state_data): result = json_normalize(series, "counties") tm.assert_index_equal(result.index, idx.repeat([3, 2])) + def test_json_normalize_int_key_with_record_path(self): + # 63019 + data = [ + { + "a": 1, + 12: "meta_value_1", + "nested": [{"b": 2, "c": 3}], + }, + { + "a": 6, + 12: "meta_value_2", + "nested": [{"b": 7, "c": 8}], + }, + ] + + result = json_normalize(data, record_path=["nested"], meta=[12, "a"]) + + expected_data = { + "b": [2, 7], + "c": [3, 8], + 12: ["meta_value_1", "meta_value_2"], + "a": [1, 6], + } + expected_columns = ["b", "c", 12, "a"] + expected = DataFrame(expected_data, columns=expected_columns) + + tm.assert_frame_equal(result, expected) + class TestNestedToRecord: def test_flat_stays_flat(self): From 1451c8671bda1f71b3a14f12ae7483e470820bfa Mon Sep 17 00:00:00 2001 From: dongwonmoon Date: Sat, 8 Nov 2025 00:34:15 +0900 Subject: [PATCH 2/4] BUG: Fix TypeError in json_normalize with non-str meta key and record_path --- pandas/tests/io/json/test_normalize.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/tests/io/json/test_normalize.py b/pandas/tests/io/json/test_normalize.py index 07a4ff1ec472a..691c1765c59f0 100644 --- a/pandas/tests/io/json/test_normalize.py +++ b/pandas/tests/io/json/test_normalize.py @@ -618,6 +618,7 @@ def test_json_normalize_int_key_with_record_path(self): } expected_columns = ["b", "c", 12, "a"] expected = DataFrame(expected_data, columns=expected_columns) + expected["a"] = expected["a"].astype(object) tm.assert_frame_equal(result, expected) From 84233a2917f047feb2fe4364596537aa882001d9 Mon Sep 17 00:00:00 2001 From: dongwonmoon Date: Sat, 8 Nov 2025 00:34:33 +0900 Subject: [PATCH 3/4] BUG: Fix TypeError in json_normalize with non-str meta key and record_path --- doc/source/whatsnew/v3.0.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 6b78f63f92988..b8c5c442a4d32 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -1175,6 +1175,7 @@ I/O - Fix bug in ``on_bad_lines`` callable when returning too many fields: now emits ``ParserWarning`` and truncates extra fields regardless of ``index_col`` (:issue:`61837`) - Bug in :func:`pandas.json_normalize` inconsistently handling non-dict items in ``data`` when ``max_level`` was set. The function will now raise a ``TypeError`` if ``data`` is a list containing non-dict items (:issue:`62829`) +- Bug in :func:`pandas.json_normalize` raising ``TypeError`` when ``meta`` contained a non-string key (e.g., ``int``) and ``record_path`` was specified, which was inconsistent with the behavior when ``record_path`` was ``None`` (:issue:`63019`) - Bug in :meth:`.DataFrame.to_json` when ``"index"`` was a value in the :attr:`DataFrame.column` and :attr:`Index.name` was ``None``. Now, this will fail with a ``ValueError`` (:issue:`58925`) - Bug in :meth:`.io.common.is_fsspec_url` not recognizing chained fsspec URLs (:issue:`48978`) - Bug in :meth:`DataFrame._repr_html_` which ignored the ``"display.float_format"`` option (:issue:`59876`) From 9b2efe22ed0f9c073ca8f7af5739f7998d547706 Mon Sep 17 00:00:00 2001 From: dongwonmoon Date: Sat, 8 Nov 2025 10:07:29 +0900 Subject: [PATCH 4/4] MNT: Add comments explaining meta_keys logic in json_normalize --- pandas/io/json/_normalize.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pandas/io/json/_normalize.py b/pandas/io/json/_normalize.py index 627d19de84f72..3dec7349271d9 100644 --- a/pandas/io/json/_normalize.py +++ b/pandas/io/json/_normalize.py @@ -555,8 +555,12 @@ def _pull_records(js: dict[str, Any], spec: list | str) -> list: meta_keys = [] for val in _meta: if len(val) == 1: + # Simple path: [12] -> 12 (preserves int type for consistency) + # Use the key directly, avoiding sep.join meta_keys.append(val[0]) else: + # Nested path: ['info', 'governor'] -> "info.governor" + # Must join, converting all parts to str to avoid TypeError meta_keys.append(sep.join(str(x) for x in val)) def _recursive_extract(data, path, seen_meta, level: int = 0) -> None: