From 15027a21d3b3b0430315550e77dc602bec6d3f3d Mon Sep 17 00:00:00 2001 From: xiaohuanlin Date: Sun, 11 May 2025 23:25:49 -0400 Subject: [PATCH] BUG: fix to_json serialization for Period objects --- doc/source/whatsnew/v3.0.0.rst | 1 + .../src/vendored/ujson/python/objToJSON.c | 7 +++ .../_libs/src/vendored/ujson/python/ujson.c | 54 +++++++++++++++++++ pandas/tests/io/json/test_ujson.py | 7 +++ 4 files changed, 69 insertions(+) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 6642f5855f4fe..5318d3707b85c 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -789,6 +789,7 @@ I/O - Bug in :meth:`read_stata` where the missing code for double was not recognised for format versions 105 and prior (:issue:`58149`) - Bug in :meth:`set_option` where setting the pandas option ``display.html.use_mathjax`` to ``False`` has no effect (:issue:`59884`) - Bug in :meth:`to_excel` where :class:`MultiIndex` columns would be merged to a single row when ``merge_cells=False`` is passed (:issue:`60274`) +- Bug in :meth:`to_json` period dtype was not being converted to string (:issue:`55490`) Period ^^^^^^ diff --git a/pandas/_libs/src/vendored/ujson/python/objToJSON.c b/pandas/_libs/src/vendored/ujson/python/objToJSON.c index 8342dbcd1763d..1b771ec98091e 100644 --- a/pandas/_libs/src/vendored/ujson/python/objToJSON.c +++ b/pandas/_libs/src/vendored/ujson/python/objToJSON.c @@ -62,6 +62,7 @@ int object_is_series_type(PyObject *obj); int object_is_index_type(PyObject *obj); int object_is_nat_type(PyObject *obj); int object_is_na_type(PyObject *obj); +int object_is_offset_type(PyObject *obj); typedef struct __NpyArrContext { PyObject *array; @@ -927,6 +928,12 @@ static int Dir_iterNext(JSOBJ _obj, JSONTypeContext *tc) { continue; } + // Skip the 'base' attribute for BaseOffset objects + if (object_is_offset_type(obj) && strcmp(attrStr, "base") == 0) { + Py_DECREF(attr); + continue; + } + itemValue = PyObject_GetAttr(obj, attrName); if (itemValue == NULL) { PyErr_Clear(); diff --git a/pandas/_libs/src/vendored/ujson/python/ujson.c b/pandas/_libs/src/vendored/ujson/python/ujson.c index 2ee084b9304f4..a720c5e606dfb 100644 --- a/pandas/_libs/src/vendored/ujson/python/ujson.c +++ b/pandas/_libs/src/vendored/ujson/python/ujson.c @@ -74,6 +74,7 @@ typedef struct { PyObject *type_index; PyObject *type_nat; PyObject *type_na; + PyObject *type_offset; } modulestate; #define modulestate(o) ((modulestate *)PyModule_GetState(o)) @@ -211,6 +212,26 @@ int object_is_na_type(PyObject *obj) { } return result; } + +int object_is_offset_type(PyObject *obj) { + PyObject *module = PyState_FindModule(&moduledef); + if (module == NULL) + return 0; + modulestate *state = modulestate(module); + if (state == NULL) + return 0; + PyObject *type_offset = state->type_offset; + if (type_offset == NULL) { + PyErr_Clear(); + return 0; + } + int result = PyObject_IsInstance(obj, type_offset); + if (result == -1) { + PyErr_Clear(); + return 0; + } + return result; +} #else /* Used in objToJSON.c */ int object_is_decimal_type(PyObject *obj) { @@ -345,6 +366,27 @@ int object_is_na_type(PyObject *obj) { return result; } +int object_is_offset_type(PyObject *obj) { + PyObject *module = PyImport_ImportModule("pandas._libs.tslibs.offsets"); + if (module == NULL) { + PyErr_Clear(); + return 0; + } + PyObject *type_offset = PyObject_GetAttrString(module, "BaseOffset"); + if (type_offset == NULL) { + Py_DECREF(module); + PyErr_Clear(); + return 0; + } + int result = PyObject_IsInstance(obj, type_offset); + if (result == -1) { + Py_DECREF(module); + Py_DECREF(type_offset); + PyErr_Clear(); + return 0; + } + return result; +} #endif static int module_traverse(PyObject *m, visitproc visit, void *arg) { @@ -354,6 +396,7 @@ static int module_traverse(PyObject *m, visitproc visit, void *arg) { Py_VISIT(modulestate(m)->type_index); Py_VISIT(modulestate(m)->type_nat); Py_VISIT(modulestate(m)->type_na); + Py_VISIT(modulestate(m)->type_offset); return 0; } @@ -364,6 +407,7 @@ static int module_clear(PyObject *m) { Py_CLEAR(modulestate(m)->type_index); Py_CLEAR(modulestate(m)->type_nat); Py_CLEAR(modulestate(m)->type_na); + Py_CLEAR(modulestate(m)->type_offset); return 0; } @@ -434,6 +478,16 @@ PyMODINIT_FUNC PyInit_json(void) { } else { PyErr_Clear(); } + + PyObject *mod_offset = PyImport_ImportModule("pandas._libs.tslibs.offsets"); + if (mod_offset) { + PyObject *type_offset = PyObject_GetAttrString(mod_offset, "BaseOffset"); + assert(type_offset != NULL); + modulestate(module)->type_offset = type_offset; + + Py_DECREF(mod_offset); + } + #endif /* Not vendored for now diff --git a/pandas/tests/io/json/test_ujson.py b/pandas/tests/io/json/test_ujson.py index d2bf9bdb139bd..9bc8a648f82ae 100644 --- a/pandas/tests/io/json/test_ujson.py +++ b/pandas/tests/io/json/test_ujson.py @@ -12,6 +12,7 @@ import pytest import pandas._libs.json as ujson +from pandas._libs.tslibs import offsets from pandas.compat import IS64 from pandas import ( @@ -1041,3 +1042,9 @@ def test_encode_periodindex(self): p = PeriodIndex(["2022-04-06", "2022-04-07"], freq="D") df = DataFrame(index=p) assert df.to_json() == "{}" + + def test_to_json_with_period(self): + # GH 55490 + offset = offsets.YearEnd(2021) + result = ujson.ujson_dumps(offset) + assert "base" not in result