Skip to content

Commit 02739a2

Browse files
committed
BUG: fix to_json serialization for Period objects
1 parent f496acf commit 02739a2

File tree

4 files changed

+77
-0
lines changed

4 files changed

+77
-0
lines changed

doc/source/whatsnew/v3.0.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,7 @@ I/O
788788
- Bug in :meth:`read_stata` where the missing code for double was not recognised for format versions 105 and prior (:issue:`58149`)
789789
- Bug in :meth:`set_option` where setting the pandas option ``display.html.use_mathjax`` to ``False`` has no effect (:issue:`59884`)
790790
- Bug in :meth:`to_excel` where :class:`MultiIndex` columns would be merged to a single row when ``merge_cells=False`` is passed (:issue:`60274`)
791+
- Bug in :meth:`to_json` period dtype was not being converted to string (:issue:`55490`)
791792

792793
Period
793794
^^^^^^

pandas/_libs/src/vendored/ujson/python/objToJSON.c

+7
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ int object_is_series_type(PyObject *obj);
6262
int object_is_index_type(PyObject *obj);
6363
int object_is_nat_type(PyObject *obj);
6464
int object_is_na_type(PyObject *obj);
65+
int object_is_offset_type(PyObject *obj);
6566

6667
typedef struct __NpyArrContext {
6768
PyObject *array;
@@ -927,6 +928,12 @@ static int Dir_iterNext(JSOBJ _obj, JSONTypeContext *tc) {
927928
continue;
928929
}
929930

931+
// Skip the 'base' attribute for BaseOffset objects
932+
if (object_is_offset_type(obj) && strcmp(attrStr, "base") == 0) {
933+
Py_DECREF(attr);
934+
continue;
935+
}
936+
930937
itemValue = PyObject_GetAttr(obj, attrName);
931938
if (itemValue == NULL) {
932939
PyErr_Clear();

pandas/_libs/src/vendored/ujson/python/ujson.c

+54
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ typedef struct {
7474
PyObject *type_index;
7575
PyObject *type_nat;
7676
PyObject *type_na;
77+
PyObject *type_offset;
7778
} modulestate;
7879

7980
#define modulestate(o) ((modulestate *)PyModule_GetState(o))
@@ -211,6 +212,26 @@ int object_is_na_type(PyObject *obj) {
211212
}
212213
return result;
213214
}
215+
216+
int object_is_offset_type(PyObject *obj) {
217+
PyObject *module = PyState_FindModule(&moduledef);
218+
if (module == NULL)
219+
return 0;
220+
modulestate *state = modulestate(module);
221+
if (state == NULL)
222+
return 0;
223+
PyObject *type_offset = state->type_offset;
224+
if (type_offset == NULL) {
225+
PyErr_Clear();
226+
return 0;
227+
}
228+
int result = PyObject_IsInstance(obj, type_offset);
229+
if (result == -1) {
230+
PyErr_Clear();
231+
return 0;
232+
}
233+
return result;
234+
}
214235
#else
215236
/* Used in objToJSON.c */
216237
int object_is_decimal_type(PyObject *obj) {
@@ -345,6 +366,27 @@ int object_is_na_type(PyObject *obj) {
345366
return result;
346367
}
347368

369+
int object_is_offset_type(PyObject *obj) {
370+
PyObject *module = PyImport_ImportModule("pandas._libs.tslibs.offsets");
371+
if (module == NULL) {
372+
PyErr_Clear();
373+
return 0;
374+
}
375+
PyObject *type_offset = PyObject_GetAttrString(module, "BaseOffset");
376+
if (type_offset == NULL) {
377+
Py_DECREF(module);
378+
PyErr_Clear();
379+
return 0;
380+
}
381+
int result = PyObject_IsInstance(obj, type_offset);
382+
if (result == -1) {
383+
Py_DECREF(module);
384+
Py_DECREF(type_offset);
385+
PyErr_Clear();
386+
return 0;
387+
}
388+
return result;
389+
}
348390
#endif
349391

350392
static int module_traverse(PyObject *m, visitproc visit, void *arg) {
@@ -354,6 +396,7 @@ static int module_traverse(PyObject *m, visitproc visit, void *arg) {
354396
Py_VISIT(modulestate(m)->type_index);
355397
Py_VISIT(modulestate(m)->type_nat);
356398
Py_VISIT(modulestate(m)->type_na);
399+
Py_VISIT(modulestate(m)->type_offset);
357400
return 0;
358401
}
359402

@@ -364,6 +407,7 @@ static int module_clear(PyObject *m) {
364407
Py_CLEAR(modulestate(m)->type_index);
365408
Py_CLEAR(modulestate(m)->type_nat);
366409
Py_CLEAR(modulestate(m)->type_na);
410+
Py_CLEAR(modulestate(m)->type_offset);
367411
return 0;
368412
}
369413

@@ -434,6 +478,16 @@ PyMODINIT_FUNC PyInit_json(void) {
434478
} else {
435479
PyErr_Clear();
436480
}
481+
482+
PyObject *mod_offset = PyImport_ImportModule("pandas._libs.tslibs.offsets");
483+
if (mod_offset) {
484+
PyObject *type_offset = PyObject_GetAttrString(mod_offset, "BaseOffset");
485+
assert(type_offset != NULL);
486+
modulestate(module)->type_offset = type_offset;
487+
488+
Py_DECREF(mod_offset);
489+
}
490+
437491
#endif
438492

439493
/* Not vendored for now

pandas/tests/io/json/test_pandas.py

+15
Original file line numberDiff line numberDiff line change
@@ -2079,6 +2079,21 @@ def e(self):
20792079
series = Series([_TestObject(a=1, b=2, _c=3, d=4)])
20802080
assert json.loads(series.to_json()) == {"0": {"a": 1, "b": 2, "d": 4}}
20812081

2082+
def test_to_json_with_period(self):
2083+
# GH55490
2084+
ser = Series(pd.period_range(start=2021, freq="Y", periods=1))
2085+
result = ser.to_json()
2086+
expected = (
2087+
'{"0":{"day":31,"day_of_week":4,"day_of_year":365,"dayofweek":4,'
2088+
'"dayofyear":365,"days_in_month":31,"daysinmonth":31,"end_time":1640995199999,'
2089+
'"freq":{"freqstr":"YE-DEC","kwds":{"month":12},"month":12,"n":1,'
2090+
'"name":"YE-DEC","normalize":false,"rule_code":"YE-DEC"},'
2091+
'"freqstr":"Y-DEC","hour":0,"is_leap_year":false,"minute":0,'
2092+
'"month":12,"ordinal":51,"quarter":4,"qyear":2021,"second":0,'
2093+
'"start_time":1609459200000,"week":52,"weekday":4,"weekofyear":52,"year":2021}}'
2094+
)
2095+
assert result == expected
2096+
20822097
@pytest.mark.parametrize(
20832098
"data,expected",
20842099
[

0 commit comments

Comments
 (0)