From 715a2f8df0b00c7697c27d9eb16a9544e84b81b5 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Fri, 24 Apr 2026 02:02:17 +0300 Subject: [PATCH] x.json2: fix x.json2: encoder emits embedded-struct fields out of declaration order, breaking byte-for-byte round-trip (fixes #26907) --- vlib/x/json2/encode.v | 156 ++++++++++++------ .../tests/encode_embedded_structs_test.v | 52 +++++- 2 files changed, 157 insertions(+), 51 deletions(-) diff --git a/vlib/x/json2/encode.v b/vlib/x/json2/encode.v index a25b92829..d00a661b1 100644 --- a/vlib/x/json2/encode.v +++ b/vlib/x/json2/encode.v @@ -140,6 +140,12 @@ fn (mut encoder Encoder) encode_value[T](val T) { unsafe { encoder.output.push_many(integer_val.str, integer_val.len) } } $else $if T.unaliased_typ is $struct { unsafe { + $for field in T.fields { + $if field.is_embed { + encoder.encode_struct_with_embeds(val) + return + } + } encoder.output << `{` is_first := encoder.encode_struct_fields[T](val, true, [], '') if encoder.prettify && !is_first { @@ -372,7 +378,7 @@ fn (mut encoder Encoder) encode_array[T](val T) { @[unsafe] fn (mut encoder Encoder) encode_pointer_array_item[T](item T) { - encoder.encode_struct(item) + encoder.encode_value(*item) } fn (mut encoder Encoder) encode_map[K, T](val map[K]T) { @@ -478,6 +484,12 @@ fn (mut encoder Encoder) encode_object_key(is_first bool, key string) bool { } fn (mut encoder Encoder) encode_sumtype_struct_variant[T](val T, variant_name string) { + $for field in T.fields { + $if field.is_embed { + unsafe { encoder.encode_sumtype_struct_variant_with_embeds(val, variant_name) } + return + } + } encoder.output << `{` mut is_first := unsafe { encoder.encode_struct_fields[T](val, true, [], '') } is_first = encoder.encode_object_key(is_first, '_type') @@ -660,30 +672,39 @@ fn struct_field_should_encode[T](field_info EncoderFieldInfo, val T) bool { } @[unsafe] -fn (mut encoder Encoder) encode_struct[T](val T) { +fn (mut encoder Encoder) encode_struct_with_embeds[T](val T) { encoder.output << `{` - - is_first := encoder.encode_struct_fields[T](val, true, [], '') - + is_first := encoder.encode_embedded_struct_fields[T](val, true, [], [], '') if encoder.prettify && !is_first { encoder.decrement_level() encoder.add_indent() } + encoder.output << `}` +} +@[unsafe] +fn (mut encoder Encoder) encode_sumtype_struct_variant_with_embeds[T](val T, variant_name string) { + encoder.output << `{` + mut is_first := encoder.encode_embedded_struct_fields[T](val, true, [], [], '') + is_first = encoder.encode_object_key(is_first, '_type') + encoder.encode_string(variant_name) + if encoder.prettify && !is_first { + encoder.decrement_level() + encoder.add_indent() + } encoder.output << `}` } @[unsafe] fn (mut encoder Encoder) encode_struct_fields[T](val T, was_first bool, old_used_keys []string, prefix string) bool { field_infos := encoder.cached_field_infos[T]() - mut i := 0 mut is_first := was_first mut used_keys := old_used_keys + mut i := 0 $for field in T.fields { $if !field.is_embed { field_info := field_infos[i] - mut write_field := true $if field.typ is $shared { @@ -692,30 +713,13 @@ fn (mut encoder Encoder) encode_struct_fields[T](val T, was_first bool, old_used write_field = struct_field_should_encode(field_info, field_value) if write_field { - if is_first { - if encoder.prettify { - encoder.increment_level() - } - is_first = false - } else { - encoder.output << `,` - } - if encoder.prettify { - encoder.add_indent() - } - if field_info.key_name in old_used_keys { - encoder.encode_string(prefix + field_info.key_name) + is_first = encoder.encode_object_key(is_first, prefix + + field_info.key_name) } else { - encoder.encode_string(field_info.key_name) + is_first = encoder.encode_object_key(is_first, field_info.key_name) used_keys << field_info.key_name } - - encoder.output << `:` - if encoder.prettify { - encoder.output << ` ` - } - encoder.encode_struct_field_value(field_value) } } @@ -723,30 +727,12 @@ fn (mut encoder Encoder) encode_struct_fields[T](val T, was_first bool, old_used write_field = struct_field_should_encode(field_info, val.$(field.name)) if write_field { - if is_first { - if encoder.prettify { - encoder.increment_level() - } - is_first = false - } else { - encoder.output << `,` - } - if encoder.prettify { - encoder.add_indent() - } - if field_info.key_name in old_used_keys { - encoder.encode_string(prefix + field_info.key_name) + is_first = encoder.encode_object_key(is_first, prefix + field_info.key_name) } else { - encoder.encode_string(field_info.key_name) + is_first = encoder.encode_object_key(is_first, field_info.key_name) used_keys << field_info.key_name } - - encoder.output << `:` - if encoder.prettify { - encoder.output << ` ` - } - encoder.encode_struct_field_value(val.$(field.name)) } } @@ -771,6 +757,82 @@ fn (mut encoder Encoder) encode_struct_fields[T](val T, was_first bool, old_used return is_first } +@[unsafe] +fn (mut encoder Encoder) encode_embedded_struct_fields[T](val T, was_first bool, old_used_keys []string, reserved_keys []string, prefix string) bool { + field_infos := encoder.cached_field_infos[T]() + mut is_first := was_first + mut used_keys := old_used_keys.clone() + mut i := 0 + + $for field in T.fields { + $if field.is_embed { + mut child_reserved_keys := reserved_keys.clone() + mut reserved_i := 0 + $for reserved_field in T.fields { + reserved_field_info := field_infos[reserved_i] + $if !reserved_field.is_embed { + if !reserved_field_info.is_skip { + child_reserved_keys << reserved_field_info.key_name + } + } + reserved_i++ + } + new_prefix := prefix + field.name + '.' + $if field.typ is $shared { + shared field_value := unsafe { val.$(field.name) } + rlock field_value { + is_first = encoder.encode_embedded_struct_fields(field_value, is_first, + used_keys, child_reserved_keys, new_prefix) + } + } $else { + is_first = encoder.encode_embedded_struct_fields(val.$(field.name), is_first, + used_keys, child_reserved_keys, new_prefix) + } + } $else { + field_info := field_infos[i] + mut write_field := true + $if field.typ is $shared { + shared field_value := unsafe { val.$(field.name) } + rlock field_value { + write_field = struct_field_should_encode(field_info, field_value) + if write_field { + should_prefix := field_info.key_name in used_keys + || field_info.key_name in reserved_keys + json_key := if should_prefix { + prefix + field_info.key_name + } else { + field_info.key_name + } + is_first = encoder.encode_object_key(is_first, json_key) + if !should_prefix { + used_keys << field_info.key_name + } + encoder.encode_struct_field_value(field_value) + } + } + } $else { + write_field = struct_field_should_encode(field_info, val.$(field.name)) + if write_field { + should_prefix := field_info.key_name in used_keys + || field_info.key_name in reserved_keys + json_key := if should_prefix { + prefix + field_info.key_name + } else { + field_info.key_name + } + is_first = encoder.encode_object_key(is_first, json_key) + if !should_prefix { + used_keys << field_info.key_name + } + encoder.encode_struct_field_value(val.$(field.name)) + } + } + } + i++ + } + return is_first +} + fn (mut encoder Encoder) encode_custom[T](val T) { integer_val := val.to_json() unsafe { encoder.output.push_many(integer_val.str, integer_val.len) } diff --git a/vlib/x/json2/tests/encode_embedded_structs_test.v b/vlib/x/json2/tests/encode_embedded_structs_test.v index f9ee85d92..f487a4a6f 100644 --- a/vlib/x/json2/tests/encode_embedded_structs_test.v +++ b/vlib/x/json2/tests/encode_embedded_structs_test.v @@ -33,14 +33,33 @@ struct Classifier { History } +struct RoundtripInner { + inner []f64 +} + +struct RoundtripOuter { + RoundtripInner + test f64 +} + +struct Article { + title string + description string +} + +struct ArticlePlus { + Article + perex string +} + fn test_embed() { - assert json.encode(Bar{}) == '{"a":"1","c":"3","Foo.a":1,"b":2,"Foo.Baz.c":true}' + assert json.encode(Bar{}) == '{"Foo.Baz.c":true,"Foo.a":1,"b":2,"a":"1","c":"3"}' assert json.encode(Bar{}, prettify: true) == '{ - "a": "1", - "c": "3", + "Foo.Baz.c": true, "Foo.a": 1, "b": 2, - "Foo.Baz.c": true + "a": "1", + "c": "3" }' } @@ -81,3 +100,28 @@ fn test_decode_embed_with_nested_objects() { assert decoded.history_events.len == 1 assert decoded.history_events[0].Environment.vflags == 'legacy' } + +fn test_embed_roundtrip_preserves_declaration_order() { + str := '{"inner":[1,2,3,4,5],"test":1.2}' + data := json.decode[RoundtripOuter](str)! + assert json.encode(data) == str +} + +fn test_encode_array_of_embedded_structs_preserves_declaration_order() { + list_of_object := [ + ArticlePlus{ + title: 'One good title' + description: 'this is the first' + }, + ArticlePlus{ + title: 'Other good title' + description: 'more one' + }, + ArticlePlus{ + title: 'Other good title' + description: 'more one' + perex: 'good perex' + }, + ] + assert json.encode(list_of_object) == '[{"title":"One good title","description":"this is the first","perex":""},{"title":"Other good title","description":"more one","perex":""},{"title":"Other good title","description":"more one","perex":"good perex"}]' +} -- 2.39.5