10000 gh-98306: Support JSON encoding of NaNs and infinities as null · mdickinson/cpython@07cc7dc · GitHub
[go: up one dir, main page]

Skip to content

Commit 07cc7dc

Browse files
committed
pythongh-98306: Support JSON encoding of NaNs and infinities as null
1 parent e2c4038 commit 07cc7dc
10000

File tree

7 files changed

+89
-14
lines changed

7 files changed

+89
-14
lines changed

Doc/library/json.rst

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,14 @@ Basic Usage
172172

173173
If *allow_nan* is false (default: ``True``), then it will be a
174174
:exc:`ValueError` to serialize out of range :class:`float` values (``nan``,
175-
``inf``, ``-inf``) in strict compliance of the JSON specification.
176-
If *allow_nan* is true, their JavaScript equivalents (``NaN``,
177-
``Infinity``, ``-Infinity``) will be used.
175+
``inf``, ``-inf``) in strict compliance with the JSON specification. If
176+
*allow_nan* is the string ``"null"``, NaNs and infinities will be converted
177+
to a JSON ``null``, matching the behavior of JavaScript's
178+
``JSON.stringify``. If *allow_nan* is true but not equal to ``"null"`` then
179+
NaNs and infinities are converted to non-quote-delimited strings ``NaN``,
180+
``Infinity`` and ``-Infinity`` in the JSON output. Note that this represents
181+
an extension of the JSON specification, and is not compliant with standard
182+
JSON.
178183

179184
If *indent* is a non-negative integer or string, then JSON array elements and
180185
object members will be pretty-printed with that indent level. An indent level
@@ -215,6 +220,9 @@ Basic Usage
215220
so trying to serialize multiple objects with repeated calls to
216221
:func:`dump` using the same *fp* will result in an invalid JSON file.
217222

223+
.. versionchanged:: 3.13
224+
Added support for ``allow_nan='null'``.
225+
218226
.. function:: dumps(obj, *, skipkeys=False, ensure_ascii=True, \
219227
check_circular=True, allow_nan=True, cls=None, \
220228
indent=None, separators=None, default=None, \
@@ -450,11 +458,15 @@ Encoders and Decoders
450458
prevent an infinite recursion (which would cause a :exc:`RecursionError`).
451459
Otherwise, no such check takes place.
452460

453-
If *allow_nan* is true (the default), then ``NaN``, ``Infinity``, and
454-
``-Infinity`` will be encoded as such. This behavior is not JSON
455-
specification compliant, but is consistent with most JavaScript based
456-
encoders and decoders. Otherwise, it will be a :exc:`ValueError` to encode
457-
such floats.
461+
If *allow_nan* is the string ``"null"``, then NaNs and infinities are
462+
encoded as JSON ``null`` values. This matches the behaviour of JavaScript's
463+
``JSON.stringify``. If *allow_nan* is true but not equal to ``"null"``, then
464+
``NaN``, ``Infinity``, and ``-Infinity`` will be encoded as corresponding
465+
non-quote-delimited strings in the JSON output. This is the default
466+
behaviour. This behavior represents an extension of the JSON specification,
467+
but is consistent with some JavaScript based encoders and decoders (as well
468+
as Python's own decoder). If *allow_nan* is false, it will be a
469+
:exc:`ValueError` to encode such floats.
458470

459471
If *sort_keys* is true (default: ``False``), then the output of dictionaries
460472
will be sorted by key; this is useful for regression tests to ensure that
@@ -486,6 +498,8 @@ Encoders and Decoders
486498
.. versionchanged:: 3.6
487499
All parameters are now :ref:`keyword-only <keyword-only_parameter>`.
488500

501+
.. versionchanged:: 3.13
502+
Added support for ``allow_nan='null'``.
489503

490504
.. method:: default(o)
491505

Doc/whatsnew/3.13.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,14 @@ ipaddress
279279
* Add the :attr:`ipaddress.IPv4Address.ipv6_mapped` property, which returns the IPv4-mapped IPv6 address.
280280
(Contributed by Charles Machalow in :gh:`109466`.)
281281

282+
json
283+
----
284+
285+
* Add support for ``allow_nan='null'`` when encoding to JSON. This converts
286+
floating-point infinities and NaNs to a JSON ``null``, for compatibility
287+
with ECMAScript's ``JSON.stringify``.
288+
(Contributed by Mark Dickinson in :gh:`XXXXXX`.)
289+
282290
marshal
283291
-------
284292

Lib/json/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ def dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True,
225225
"""
226226
# cached encoder
227227
if (not skipkeys and ensure_ascii and
228-
check_circular and allow_nan and
228+
check_circular< 341A /span> and allow_nan is True and
229229
cls is None and indent is None and separators is None and
230230
default is None and not sort_keys and not kw):
231231
return _default_encoder.encode(obj)

Lib/json/encoder.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,10 @@ def floatstr(o, allow_nan=self.allow_nan,
236236
else:
237237
return _repr(o)
238238

239-
if not allow_nan:
239+
if allow_nan == 'null':
240+
return 'null'
241+
242+
elif not allow_nan:
240243
raise ValueError(
241244
"Out of range float values are not JSON compliant: " +
242245
repr(o))

Lib/test/test_json/test_float.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
from test.test_json import PyTest, CTest
33

44

5+
class NotUsableAsABoolean:
6+
def __bool__(self):
7+
raise TypeError("I refuse to be interpreted as a boolean")
8+
9+
10+
511
class TestFloat:
612
def test_floats(self):
713
for num in [1617161771.7650001, math.pi, math.pi**100, math.pi**-100, 3.1]:
@@ -29,6 +35,32 @@ def test_allow_nan(self):
2935
msg = f'Out of range float values are not JSON compliant: {val}'
3036
self.assertRaisesRegex(ValueError, msg, self.dumps, [val], allow_nan=False)
3137

38+
def test_allow_nan_null(self):
39+
# when allow_nan is "null", infinities and NaNs are converted to "null"
40+
for val in [float('inf'), float('-inf'), float('nan')]:
41+
with self.subTest(val=val):
42+
out = self.dumps([val], allow_nan="null")
43+
res = self.loads(out)
44+
self.assertEqual(res, [None])
45+
46+
# and finite values are treated as normal
47+
for val in [1.25, -23, -0.0, 0.0]:
48+
with self.subTest(val=val):
49+
out = self.dumps([val], allow_nan="null")
50+
res = self.loads(out)
51+
self.assertEqual(res, [val])
52+
53+
# testing a mixture
54+
vals = [-1.3, 1e100, -math.inf, 1234, -0.0, math.nan]
55+
out = self.dumps(vals, allow_nan="null")
56+
res = self.loads(out)
57+
self.assertEqual(res, [-1.3, 1e100, None, 1234, -0.0, None])
58+
59+
def test_allow_nan_non_boolean(self):
60+
# check that exception gets propagated as expected
61+
with self.assertRaises(TypeError):
62+
self.dumps(math.inf, allow_nan=NotUsableAsABoolean())
63+
3264

3365
class TestPyFloat(TestFloat, PyTest): pass
3466
class TestCFloat(TestFloat, CTest): pass
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add support for ``allow_nan='null'`` when encoding an object to a JSON
2+
string. This converts floating-point infinities and NaNs to a JSON ``null``.

Modules/_json.c

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,13 +1209,13 @@ encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
12091209

12101210
PyEncoderObject *s;
12111211
PyObject *markers, *defaultfn, *encoder, *indent, *key_separator;
1212-
PyObject *item_separator;
1212+
PyObject *item_separator, *allow_nan_obj;
12131213
int sort_keys, skipkeys, allow_nan;
12141214

1215-
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOOUUppp:make_encoder", kwlist,
1215+
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOOUUppO:make_encoder", kwlist,
12161216
&markers, &defaultfn, &encoder, &indent,
12171217
&key_separator, &item_separator,
1218-
&sort_keys, 106B2 &skipkeys, &allow_nan))
1218+
&sort_keys, &skipkeys, &allow_nan_obj))
12191219
return NULL;
12201220

12211221
if (markers != Py_None && !PyDict_Check(markers)) {
@@ -1225,6 +1225,19 @@ encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
12251225
return NULL;
12261226
}
12271227

1228+
// allow_nan =
1229+
// 0 to disallow nans and infinities
1230+
// 1 to convert nans and infinities into corresponding JSON strings
1231+
// 2 to convert nans and infinities to a JSON null
1232+
if (PyUnicode_Check(allow_nan_obj) && _PyUnicode_Equal(allow_nan_obj, &_Py_ID(null))) {
1233+
allow_nan = 2;
1234+
} else {
1235+
allow_nan = PyObject_IsTrue(allow_nan_obj);
1236+
if (allow_nan < 0) {
1237+
return NULL;
1238+
}
1239+
}
1240+
12281241
s = (PyEncoderObject *)type->tp_alloc(type, 0);
12291242
if (s == NULL)
12301243
return NULL;
@@ -1314,7 +1327,10 @@ encoder_encode_float(PyEncoderObject *s, PyObject *obj)
13141327
);
13151328
return NULL;
13161329
}
1317-
if (i > 0) {
1330+
else if (s->allow_nan == 2) {
1331+
return PyUnicode_FromString("null");
1332+
}
1333+
else if (i > 0) {
13181334
return PyUnicode_FromString("Infinity");
13191335
}
13201336
else if (i < 0) {

0 commit comments

Comments
 (0)
0