Skip to content

Commit bfacb48

Browse files
committed
Fix handling of nested objects when running with object_hook or object_pairs_hook in CallScannerNode
1 parent d0bf0a9 commit bfacb48

2 files changed

Lines changed: 34 additions & 2 deletions

File tree

graalpython/com.oracle.graal.python.test/src/tests/test_json.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved.
22
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
33
#
44
# The Universal Permissive License (UPL), Version 1.0
@@ -88,3 +88,11 @@ def test_encode_surrogate(self):
8888
assert s == '{"foo": "\\uda6a"}'
8989
s = json.dumps({'foo': "\uda6a"}, ensure_ascii=False)
9090
assert s == '{"foo": "\uda6a"}'
91+
92+
def test_object_hook_nested(self):
93+
def hook(obj):
94+
return "hooked"
95+
96+
assert json.loads('{"outer": {"inner": {"leaf": 1}}}', object_hook=hook) == "hooked"
97+
assert json.loads('{"outer": {"inner": {"leaf": 1}}}', object_pairs_hook=hook) == "hooked"
98+

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/json/JSONScannerBuiltins.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* Copyright (c) 2020, 2025, Oracle and/or its affiliates.
1+
/* Copyright (c) 2020, 2026, Oracle and/or its affiliates.
22
* Copyright (C) 1996-2020 Python Software Foundation
33
*
44
* Licensed under the PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
@@ -445,6 +445,7 @@ private Object scanOnceUnicode(VirtualFrame frame, BoundaryCallData boundaryCall
445445
final Object value;
446446
ScannerState nextState = null;
447447
if (state == ScannerState.dict) {
448+
/* scanner is currently inside a dictionary */
448449
int c;
449450
if (idx >= length || (c = codePointAtIndexNode.execute(string, idx)) != '"' && (c != '}')) {
450451
throw decodeError(frame, boundaryCallData, inliningTarget, errorProfile, this, string, idx, ErrorMessages.EXPECTING_PROP_NAME_ECLOSED_IN_DBL_QUOTES);
@@ -457,6 +458,17 @@ private Object scanOnceUnicode(VirtualFrame frame, BoundaryCallData boundaryCall
457458
Object topOfStack = stack.pop();
458459
TruffleString parentKey = null;
459460
final Object dict;
461+
/*
462+
* If no hooks are present, the stack contains only the parent dicts or
463+
* lists the current object is nested in. If a objectHook or objectPairsHook
464+
* is present, the stack also stores the property keys of nested dicts. For
465+
* example, when parsing the innermost dict of '{"a":{"b":{"c":"d"}}}', the
466+
* stack will contain (from bottom to top) [<dictStorage>, <dictStorage>,
467+
* "a", <dictStorage>, "b"]. This is necessary so after finishing parsing a
468+
* dict, we can call the hook and replace it with the hook's return value in
469+
* the parent dict, i.e. when in the previous example we finish parsing
470+
* '{"c":"d"}', we effectively execute parent["b"] = objectHook({"c":"d"}).
471+
*/
460472
if (hasPairsHook) {
461473
if (topOfStack instanceof TruffleString) {
462474
parentKey = (TruffleString) topOfStack;
@@ -484,6 +496,9 @@ private Object scanOnceUnicode(VirtualFrame frame, BoundaryCallData boundaryCall
484496
nextState = parentKey == null ? ScannerState.list : ScannerState.dict;
485497
if (nextState == ScannerState.dict) {
486498
Object parent = stack.peek();
499+
if (parent instanceof TruffleString) {
500+
parent = stack.get(stack.size() - 2);
501+
}
487502
if (hasPairsHook) {
488503
currentPairsStorage = (ObjectSequenceStorage) parent;
489504
currentPairsStorage.setObjectItemNormalized(currentPairsStorage.length() - 1, PFactory.createTuple(language, new Object[]{parentKey, dict}));
@@ -518,6 +533,7 @@ private Object scanOnceUnicode(VirtualFrame frame, BoundaryCallData boundaryCall
518533
substringByteIndexNode,
519534
appendCodePointNode,
520535
appendSubstringByteIndexNode, builderToStringNode);
536+
/* force hash computation */
521537
hashCodeNode.execute(newKey, TS_ENCODING);
522538
TruffleString key = memoPutIfAbsent(memo, newKey);
523539
if (key == null) {
@@ -624,12 +640,20 @@ private Object scanOnceUnicode(VirtualFrame frame, BoundaryCallData boundaryCall
624640
assert nextState == ScannerState.dict;
625641
currentPairsStorage = (ObjectSequenceStorage) value;
626642
if (state == ScannerState.dict) {
643+
/*
644+
* save the associated propertyKey so we can replace the current
645+
* value with the hook's result later
646+
*/
627647
stack.add(propertyKey);
628648
}
629649
} else {
630650
assert nextState == ScannerState.dict;
631651
currentDictStorage = (EconomicMapStorage) ((PDict) value).getDictStorage();
632652
if (hasObjectHook && state == ScannerState.dict) {
653+
/*
654+
* save the associated propertyKey so we can replace the current
655+
* value with the hook's result later
656+
*/
633657
stack.add(propertyKey);
634658
}
635659
}

0 commit comments

Comments
 (0)