https://github.com/python/cpython/commit/b7e1d51e6b0764dd51e97c2b539e9633028afb2d
commit: b7e1d51e6b0764dd51e97c2b539e9633028afb2d
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-04-09T13:50:44+02:00
summary:

gh-148241: Fix json serialization for str subclasses (#148249)

Fix json serialization: no longer call str(obj) on str subclasses.

Replace PyUnicodeWriter_WriteStr() with PyUnicodeWriter_WriteASCII()
and private _PyUnicodeWriter_WriteStr().

files:
A Misc/NEWS.d/next/Library/2026-04-08-14-19-17.gh-issue-148241.fO_QT4.rst
M Lib/test/test_json/test_dump.py
M Lib/test/test_json/test_encode_basestring_ascii.py
M Lib/test/test_json/test_enum.py
M Modules/_json.c

diff --git a/Lib/test/test_json/test_dump.py b/Lib/test/test_json/test_dump.py
index 9880698455ca5e..850e5ceeba0c89 100644
--- a/Lib/test/test_json/test_dump.py
+++ b/Lib/test/test_json/test_dump.py
@@ -77,6 +77,36 @@ def __lt__(self, o):
         d[1337] = "true.dat"
         self.assertEqual(self.dumps(d, sort_keys=True), '{"1337": "true.dat"}')
 
+    def test_dumps_str_subclass(self):
+        # Don't call obj.__str__() on str subclasses
+
+        # str subclass which returns a different string on str(obj)
+        class StrSubclass(str):
+            def __str__(self):
+                return "StrSubclass"
+
+        obj = StrSubclass('ascii')
+        self.assertEqual(self.dumps(obj), '"ascii"')
+        self.assertEqual(self.dumps([obj]), '["ascii"]')
+        self.assertEqual(self.dumps({'key': obj}), '{"key": "ascii"}')
+
+        obj = StrSubclass('escape\n')
+        self.assertEqual(self.dumps(obj), '"escape\\n"')
+        self.assertEqual(self.dumps([obj]), '["escape\\n"]')
+        self.assertEqual(self.dumps({'key': obj}), '{"key": "escape\\n"}')
+
+        obj = StrSubclass('nonascii:é')
+        self.assertEqual(self.dumps(obj, ensure_ascii=False),
+                         '"nonascii:é"')
+        self.assertEqual(self.dumps([obj], ensure_ascii=False),
+                         '["nonascii:é"]')
+        self.assertEqual(self.dumps({'key': obj}, ensure_ascii=False),
+                         '{"key": "nonascii:é"}')
+        self.assertEqual(self.dumps(obj), '"nonascii:\\u00e9"')
+        self.assertEqual(self.dumps([obj]), '["nonascii:\\u00e9"]')
+        self.assertEqual(self.dumps({'key': obj}),
+                         '{"key": "nonascii:\\u00e9"}')
+
 
 class TestPyDump(TestDump, PyTest): pass
 
diff --git a/Lib/test/test_json/test_encode_basestring_ascii.py 
b/Lib/test/test_json/test_encode_basestring_ascii.py
index c90d3e968e5ef9..1b5dfcfde01d11 100644
--- a/Lib/test/test_json/test_encode_basestring_ascii.py
+++ b/Lib/test/test_json/test_encode_basestring_ascii.py
@@ -3,6 +3,11 @@
 from test.support import bigaddrspacetest
 
 
+# str subclass which returns a different string on str(obj)
+class StrSubclass(str):
+    def __str__(self):
+        return "StrSubclass"
+
 CASES = [
     
('/\\"\ucafe\ubabe\uab98\ufcde\ubcda\uef4a\x08\x0c\n\r\t`1~!@#$%^&*()_+-=[]{}|;:\',./<>?',
 
'"/\\\\\\"\\ucafe\\ubabe\\uab98\\ufcde\\ubcda\\uef4a\\b\\f\\n\\r\\t`1~!@#$%^&*()_+-=[]{}|;:\',./<>?"'),
     ('\u0123\u4567\u89ab\ucdef\uabcd\uef4a', 
'"\\u0123\\u4567\\u89ab\\ucdef\\uabcd\\uef4a"'),
@@ -14,6 +19,8 @@
     ('\U0001d120', '"\\ud834\\udd20"'),
     ('\u03b1\u03a9', '"\\u03b1\\u03a9"'),
     ("`1~!@#$%^&*()_+-={':[,]}|;.</>?", '"`1~!@#$%^&*()_+-={\':[,]}|;.</>?"'),
+    # Don't call obj.__str__() on str subclasses
+    (StrSubclass('ascii'), '"ascii"'),
 ]
 
 class TestEncodeBasestringAscii:
diff --git a/Lib/test/test_json/test_enum.py b/Lib/test/test_json/test_enum.py
index 196229897bd6e3..518c3e11200659 100644
--- a/Lib/test/test_json/test_enum.py
+++ b/Lib/test/test_json/test_enum.py
@@ -31,6 +31,9 @@ class WeirdNum(float, Enum):
     neg_inf = NEG_INF
     nan = NAN
 
+class StringEnum(str, Enum):
+    COLOR = "color"
+
 class TestEnum:
 
     def test_floats(self):
@@ -116,5 +119,11 @@ def test_dict_values(self):
         self.assertEqual(nd['j'], NEG_INF)
         self.assertTrue(isnan(nd['n']))
 
+    def test_str_enum(self):
+        obj = StringEnum.COLOR
+        self.assertEqual(self.dumps(obj), '"color"')
+        self.assertEqual(self.dumps([obj]), '["color"]')
+        self.assertEqual(self.dumps({'key': obj}), '{"key": "color"}')
+
 class TestPyEnum(TestEnum, PyTest): pass
 class TestCEnum(TestEnum, CTest): pass
diff --git 
a/Misc/NEWS.d/next/Library/2026-04-08-14-19-17.gh-issue-148241.fO_QT4.rst 
b/Misc/NEWS.d/next/Library/2026-04-08-14-19-17.gh-issue-148241.fO_QT4.rst
new file mode 100644
index 00000000000000..bf8d0e4382e6f6
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-04-08-14-19-17.gh-issue-148241.fO_QT4.rst
@@ -0,0 +1,2 @@
+:mod:`json`: Fix serialization: no longer call ``str(obj)`` on :class:`str`
+subclasses. Patch by Victor Stinner.
diff --git a/Modules/_json.c b/Modules/_json.c
index 36614138501e79..a20466de8c50e4 100644
--- a/Modules/_json.c
+++ b/Modules/_json.c
@@ -258,7 +258,10 @@ write_escaped_ascii(PyUnicodeWriter *writer, PyObject 
*pystr)
         if (PyUnicodeWriter_WriteChar(writer, '"') < 0) {
             return -1;
         }
-        if (PyUnicodeWriter_WriteStr(writer, pystr) < 0) {
+        // gh-148241: Avoid PyUnicodeWriter_WriteStr() which calls str(obj)
+        // on str subclasses
+        assert(PyUnicode_IS_ASCII(pystr));
+        if (PyUnicodeWriter_WriteASCII(writer, input, input_chars) < 0) {
             return -1;
         }
         return PyUnicodeWriter_WriteChar(writer, '"');
@@ -399,7 +402,9 @@ write_escaped_unicode(PyUnicodeWriter *writer, PyObject 
*pystr)
         if (PyUnicodeWriter_WriteChar(writer, '"') < 0) {
             return -1;
         }
-        if (PyUnicodeWriter_WriteStr(writer, pystr) < 0) {
+        // gh-148241: Avoid PyUnicodeWriter_WriteStr() which calls str(obj)
+        // on str subclasses
+        if (_PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, pystr) < 0) {
             return -1;
         }
         return PyUnicodeWriter_WriteChar(writer, '"');

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to