Skip to content

Commit 4b445ce

Browse files
authored
x.json2: fix decoding wrong value depending on field order (fix #26503) (made with copilot) (#26571)
1 parent 0914c7d commit 4b445ce

2 files changed

Lines changed: 90 additions & 1 deletion

File tree

‎vlib/x/json2/decode.v‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,27 @@ fn (mut decoder Decoder) decode_value[T](mut val T) ! {
460460
// field loop
461461
for {
462462
if current_field_info == unsafe { nil } {
463-
decoder.current_node = decoder.current_node.next // skip value
463+
// The key doesn't match any field in the struct, skip the entire value
464+
// including all nested objects/arrays
465+
decoder.current_node = decoder.current_node.next // move to value node
466+
467+
if decoder.current_node != unsafe { nil } {
468+
// Calculate the end position of this value
469+
value_end := decoder.current_node.value.position +
470+
decoder.current_node.value.length
471+
472+
// Skip all nodes that belong to this value (nested content)
473+
for {
474+
if decoder.current_node == unsafe { nil } {
475+
break
476+
}
477+
// Check if current node is still within the value's boundaries
478+
if decoder.current_node.value.position >= value_end {
479+
break
480+
}
481+
decoder.current_node = decoder.current_node.next
482+
}
483+
}
464484

465485
break
466486
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import x.json2
2+
3+
// Test for issue #26503: Decoder incorrectly reads nested object fields
4+
// When a required field appears after unmatched fields with nested objects/arrays,
5+
// the decoder was incorrectly picking up values from nested structures.
6+
7+
struct Foo {
8+
id string @[required]
9+
title string @[required]
10+
}
11+
12+
fn test_decode_with_nested_objects_field_order() {
13+
// Test case 1: required fields appear before the nested array
14+
s1 := '{"id":"sss","title":"ttt","thumb":[{ "url":"i1.jpg","id":"000"}]}'
15+
f1 := json2.decode[Foo](s1)!
16+
17+
assert f1.id == 'sss', 'f1.id should be "sss" but got "${f1.id}"'
18+
assert f1.title == 'ttt', 'f1.title should be "ttt" but got "${f1.title}"'
19+
20+
// Test case 2: required fields appear after the nested array
21+
s2 := '{"title":"ttt","thumb":[{ "url":"i1.jpg","id":"000"}],"id":"sss"}'
22+
f2 := json2.decode[Foo](s2)!
23+
24+
assert f2.id == 'sss', 'f2.id should be "sss" but got "${f2.id}"'
25+
assert f2.title == 'ttt', 'f2.title should be "ttt" but got "${f2.title}"'
26+
27+
// Test case 3: nested array appears between required fields
28+
s3 := '{"id":"sss","thumb":[{ "url":"i1.jpg","id":"000"}],"title":"ttt"}'
29+
f3 := json2.decode[Foo](s3)!
30+
31+
assert f3.id == 'sss', 'f3.id should be "sss" but got "${f3.id}"'
32+
assert f3.title == 'ttt', 'f3.title should be "ttt" but got "${f3.title}"'
33+
}
34+
35+
fn test_decode_with_deeply_nested_objects() {
36+
// Test with deeply nested structures to ensure complete skipping
37+
s := '{"id":"outer","data":{"nested":{"deep":{"id":"inner","title":"inner_title"}}},"title":"outer_title"}'
38+
f := json2.decode[Foo](s)!
39+
40+
assert f.id == 'outer', 'id should be "outer" but got "${f.id}"'
41+
assert f.title == 'outer_title', 'title should be "outer_title" but got "${f.title}"'
42+
}
43+
44+
fn test_decode_with_multiple_nested_arrays() {
45+
// Test with multiple nested arrays to ensure complete skipping
46+
s := '{"id":"correct","items":[{"id":"a"},{"id":"b"}],"more":[{"id":"c"}],"title":"correct_title"}'
47+
f := json2.decode[Foo](s)!
48+
49+
assert f.id == 'correct', 'id should be "correct" but got "${f.id}"'
50+
assert f.title == 'correct_title', 'title should be "correct_title" but got "${f.title}"'
51+
}
52+
53+
struct BarWithOptional {
54+
id string @[required]
55+
title ?string
56+
extra string
57+
}
58+
59+
fn test_decode_optional_fields_with_nested() {
60+
// Test optional fields work correctly with nested structures
61+
s := '{"id":"main","nested":{"title":"nested_title"},"extra":"extra_value"}'
62+
b := json2.decode[BarWithOptional](s)!
63+
64+
assert b.id == 'main', 'id should be "main" but got "${b.id}"'
65+
assert b.extra == 'extra_value', 'extra should be "extra_value" but got "${b.extra}"'
66+
if title := b.title {
67+
assert false, 'title should be none but got "${title}"'
68+
}
69+
}

0 commit comments

Comments
 (0)