From 4b445ced19c155b029d55f2228069c3e467e2e7e Mon Sep 17 00:00:00 2001 From: Carlos Esquerdo Bernat Date: Tue, 10 Feb 2026 12:22:38 +0100 Subject: [PATCH] x.json2: fix decoding wrong value depending on field order (fix #26503) (made with copilot) (#26571) --- vlib/x/json2/decode.v | 22 +++++- .../decode_nested_object_same_kay_test.v | 69 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 vlib/x/json2/tests/decode_nested_object_same_kay_test.v diff --git a/vlib/x/json2/decode.v b/vlib/x/json2/decode.v index 17fbd0a3b..27488ce5e 100644 --- a/vlib/x/json2/decode.v +++ b/vlib/x/json2/decode.v @@ -460,7 +460,27 @@ fn (mut decoder Decoder) decode_value[T](mut val T) ! { // field loop for { if current_field_info == unsafe { nil } { - decoder.current_node = decoder.current_node.next // skip value + // The key doesn't match any field in the struct, skip the entire value + // including all nested objects/arrays + decoder.current_node = decoder.current_node.next // move to value node + + if decoder.current_node != unsafe { nil } { + // Calculate the end position of this value + value_end := decoder.current_node.value.position + + decoder.current_node.value.length + + // Skip all nodes that belong to this value (nested content) + for { + if decoder.current_node == unsafe { nil } { + break + } + // Check if current node is still within the value's boundaries + if decoder.current_node.value.position >= value_end { + break + } + decoder.current_node = decoder.current_node.next + } + } break } diff --git a/vlib/x/json2/tests/decode_nested_object_same_kay_test.v b/vlib/x/json2/tests/decode_nested_object_same_kay_test.v new file mode 100644 index 000000000..88ae39359 --- /dev/null +++ b/vlib/x/json2/tests/decode_nested_object_same_kay_test.v @@ -0,0 +1,69 @@ +import x.json2 + +// Test for issue #26503: Decoder incorrectly reads nested object fields +// When a required field appears after unmatched fields with nested objects/arrays, +// the decoder was incorrectly picking up values from nested structures. + +struct Foo { + id string @[required] + title string @[required] +} + +fn test_decode_with_nested_objects_field_order() { + // Test case 1: required fields appear before the nested array + s1 := '{"id":"sss","title":"ttt","thumb":[{ "url":"i1.jpg","id":"000"}]}' + f1 := json2.decode[Foo](s1)! + + assert f1.id == 'sss', 'f1.id should be "sss" but got "${f1.id}"' + assert f1.title == 'ttt', 'f1.title should be "ttt" but got "${f1.title}"' + + // Test case 2: required fields appear after the nested array + s2 := '{"title":"ttt","thumb":[{ "url":"i1.jpg","id":"000"}],"id":"sss"}' + f2 := json2.decode[Foo](s2)! + + assert f2.id == 'sss', 'f2.id should be "sss" but got "${f2.id}"' + assert f2.title == 'ttt', 'f2.title should be "ttt" but got "${f2.title}"' + + // Test case 3: nested array appears between required fields + s3 := '{"id":"sss","thumb":[{ "url":"i1.jpg","id":"000"}],"title":"ttt"}' + f3 := json2.decode[Foo](s3)! + + assert f3.id == 'sss', 'f3.id should be "sss" but got "${f3.id}"' + assert f3.title == 'ttt', 'f3.title should be "ttt" but got "${f3.title}"' +} + +fn test_decode_with_deeply_nested_objects() { + // Test with deeply nested structures to ensure complete skipping + s := '{"id":"outer","data":{"nested":{"deep":{"id":"inner","title":"inner_title"}}},"title":"outer_title"}' + f := json2.decode[Foo](s)! + + assert f.id == 'outer', 'id should be "outer" but got "${f.id}"' + assert f.title == 'outer_title', 'title should be "outer_title" but got "${f.title}"' +} + +fn test_decode_with_multiple_nested_arrays() { + // Test with multiple nested arrays to ensure complete skipping + s := '{"id":"correct","items":[{"id":"a"},{"id":"b"}],"more":[{"id":"c"}],"title":"correct_title"}' + f := json2.decode[Foo](s)! + + assert f.id == 'correct', 'id should be "correct" but got "${f.id}"' + assert f.title == 'correct_title', 'title should be "correct_title" but got "${f.title}"' +} + +struct BarWithOptional { + id string @[required] + title ?string + extra string +} + +fn test_decode_optional_fields_with_nested() { + // Test optional fields work correctly with nested structures + s := '{"id":"main","nested":{"title":"nested_title"},"extra":"extra_value"}' + b := json2.decode[BarWithOptional](s)! + + assert b.id == 'main', 'id should be "main" but got "${b.id}"' + assert b.extra == 'extra_value', 'extra should be "extra_value" but got "${b.extra}"' + if title := b.title { + assert false, 'title should be none but got "${title}"' + } +} -- 2.39.5