From 15027a21d3b3b0430315550e77dc602bec6d3f3d Mon Sep 17 00:00:00 2001
From: xiaohuanlin <xiaohuanlin1993@gmail.com>
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