gh-146440: Add "array_hook" parameter to JSON decoders#146441
gh-146440: Add "array_hook" parameter to JSON decoders#146441jsbueno wants to merge 13 commits intopython:mainfrom
Conversation
Add array_hook parameter, to both
Python and C implementations
|
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
Misc/NEWS.d/next/Library/2026-03-26-02-06-52.gh-issue-146440.HXjhQO.rst
Outdated
Show resolved
Hide resolved
serhiy-storchaka
left a comment
There was a problem hiding this comment.
This change is simple enough, and I have no strong objections, although I do not see an immerse need of this feature.
Please add a What's New entry.
Lib/json/__init__.py
Outdated
|
|
||
|
|
||
| _default_decoder = JSONDecoder(object_hook=None, object_pairs_hook=None) | ||
| _default_decoder = JSONDecoder(object_hook=None, object_pairs_hook=None, array_hook=None) |
There was a problem hiding this comment.
This is not needed. Actually, passing object_hook and object_pairs_hook is also not needed.
There was a problem hiding this comment.
I've removed the ther default parameters, then.
Misc/NEWS.d/next/Library/2026-03-26-02-06-52.gh-issue-146440.HXjhQO.rst
Outdated
Show resolved
Hide resolved
Misc/NEWS.d/next/Library/2026-03-26-02-06-52.gh-issue-146440.HXjhQO.rst
Outdated
Show resolved
Hide resolved
| def loads(s, *, cls=None, object_hook=None, parse_float=None, | ||
| parse_int=None, parse_constant=None, object_pairs_hook=None, **kw): | ||
| parse_int=None, parse_constant=None, object_pairs_hook=None, | ||
| array_hook=None, **kw): |
There was a problem hiding this comment.
Can you take this opportunity to fix the indentation? Add 2 spaces.
| raise JSONDecodeError("Illegal trailing comma before end of array", s, comma_idx) | ||
|
|
||
| if array_hook is not None: | ||
| return array_hook(values), end |
There was a problem hiding this comment.
I would prefer to have a single return statement, so reuse your code from below:
| return array_hook(values), end | |
| values = array_hook(values) |
| ``array_hook`` is an optional function that will be called with the result | ||
| of any literal array decode (a ``list``). The return value of this function will | ||
| be used instead of the ``list``. This feature can be used along | ||
| ``object_pairs_hook`` to customize the resulting data structure - for example, | ||
| by setting that to ``frozendict`` and ``array_hook`` to ``tuple``, one can get | ||
| a deep immutable data structute from any JSON data. |
There was a problem hiding this comment.
Reformatted to fit into 80 columns:
| ``array_hook`` is an optional function that will be called with the result | |
| of any literal array decode (a ``list``). The return value of this function will | |
| be used instead of the ``list``. This feature can be used along | |
| ``object_pairs_hook`` to customize the resulting data structure - for example, | |
| by setting that to ``frozendict`` and ``array_hook`` to ``tuple``, one can get | |
| a deep immutable data structute from any JSON data. | |
| ``array_hook`` is an optional function that will be called with the | |
| result of any literal array decode (a ``list``). The return value of | |
| this function will be used instead of the ``list``. This feature can | |
| be used along ``object_pairs_hook`` to customize the resulting data | |
| structure - for example, by setting that to ``frozendict`` and | |
| ``array_hook`` to ``tuple``, one can get a deep immutable data | |
| structute from any JSON data. |
| @@ -0,0 +1,6 @@ | |||
| :mod:`json`: Add the *array_hook* parameter to :func:`~json.load` and | |||
| :func:`~json.loads` functions: | |||
| Allow a callback for JSON literal array types to customize Python lists in the | |||
There was a problem hiding this comment.
| Allow a callback for JSON literal array types to customize Python lists in the | |
| allow a callback for JSON literal array types to customize Python lists in the |
| goto bail; | ||
| } | ||
| *next_idx_ptr = idx + 1; | ||
| /* if array_hook is not None: rval = array_hook(rval) */ |
There was a problem hiding this comment.
Your pseudo-code doesn't describe correctly the code below:
| /* if array_hook is not None: rval = array_hook(rval) */ | |
| /* if array_hook is not None: return array_hook(rval) */ |
| } | ||
| *next_idx_ptr = idx + 1; | ||
| /* if array_hook is not None: rval = array_hook(rval) */ | ||
| if (s->array_hook != Py_None) { |
There was a problem hiding this comment.
| if (s->array_hook != Py_None) { | |
| if (!Py_IsNone(s->array_hook)) { |
| if (s->array_hook == NULL) | ||
| goto bail; |
There was a problem hiding this comment.
New code should follow PEP 7:
| if (s->array_hook == NULL) | |
| goto bail; | |
| if (s->array_hook == NULL) { | |
| goto bail; | |
| } |
| def test_array_hook(self): | ||
| s = '[1, 2, 3]' | ||
|
|
||
| t = self.loads(s, array_hook=tuple) | ||
| self.assertEqual(t, (1, 2, 3)) | ||
| self.assertEqual(type(t), tuple) | ||
| # Array in inner structure | ||
| s = '{"xkd": [1, 2, 3]}' | ||
| p = {"xkd": (1, 2, 3)} | ||
| data = self.loads(s, array_hook=tuple) | ||
| self.assertEqual(data, p) | ||
| self.assertEqual(type(data["xkd"]), tuple) | ||
|
|
||
| self.assertEqual(self.loads('[]', array_hook=tuple), ()) |
There was a problem hiding this comment.
The new documentation suggests object_hook=frozendict with array_hook=tuple, so IMO it's is a good opportunity to test them. I suggest a more elaborated test also with nested lists:
| def test_array_hook(self): | |
| s = '[1, 2, 3]' | |
| t = self.loads(s, array_hook=tuple) | |
| self.assertEqual(t, (1, 2, 3)) | |
| self.assertEqual(type(t), tuple) | |
| # Array in inner structure | |
| s = '{"xkd": [1, 2, 3]}' | |
| p = {"xkd": (1, 2, 3)} | |
| data = self.loads(s, array_hook=tuple) | |
| self.assertEqual(data, p) | |
| self.assertEqual(type(data["xkd"]), tuple) | |
| self.assertEqual(self.loads('[]', array_hook=tuple), ()) | |
| def test_array_hook(self): | |
| s = '[1, 2, 3]' | |
| t = self.loads(s, array_hook=tuple) | |
| self.assertEqual(t, (1, 2, 3)) | |
| self.assertEqual(type(t), tuple) | |
| # Nested array in inner structure with object_hook | |
| s = '{"xkd": [[1], [2], [3]]}' | |
| p = frozendict(xkd=((1,), (2,), (3,))) | |
| data = self.loads(s, object_hook=frozendict, array_hook=tuple) | |
| self.assertEqual(data, p) | |
| self.assertEqual(type(data), frozendict) | |
| self.assertEqual(type(data["xkd"]), tuple) | |
| for item in data["xkd"]: | |
| self.assertEqual(type(item), tuple) | |
| self.assertEqual(self.loads('[]', array_hook=tuple), ()) |
| of service attacks. | ||
|
|
||
| .. function:: loads(s, *, cls=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, object_pairs_hook=None, **kw) | ||
| .. versionchanged:: 3.15 |
There was a problem hiding this comment.
| .. versionchanged:: 3.15 | |
| .. versionchanged:: next |
Co-authored-by: Victor Stinner <vstinner@python.org>
Add array_hook parameter to json.load and json.loads
📚 Documentation preview 📚: https://cpython-previews--146441.org.readthedocs.build/