From e3d4433d5df208c82b14168a45ab4ab5eeebde0b Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Tue, 21 Apr 2026 05:23:51 +0300 Subject: [PATCH] x.json2: fix encoder omitting _type discriminator field for sumtype values (fixes #26904) --- vlib/x/json2/decode_sumtype.v | 101 +++++++++++++++--- vlib/x/json2/encode.v | 56 +++++++++- vlib/x/json2/tests/encode_struct_test.v | 6 +- .../json_test.v | 18 +++- .../json_test.v | 18 +++- 5 files changed, 170 insertions(+), 29 deletions(-) diff --git a/vlib/x/json2/decode_sumtype.v b/vlib/x/json2/decode_sumtype.v index 9fb7a710f..fbc46c590 100644 --- a/vlib/x/json2/decode_sumtype.v +++ b/vlib/x/json2/decode_sumtype.v @@ -2,6 +2,15 @@ module json2 import time +struct SumtypeTimeValue { + typ string @[json: '_type'; required] + value i64 @[required] +} + +fn sumtype_variant_name(type_name string) string { + return type_name.all_after_last('.') +} + fn copy_type[T](_t T) T { return T{} } @@ -11,7 +20,11 @@ fn (mut decoder Decoder) get_decoded_sumtype_workaround[T](initialized_sumtype T resolved_sumtype := initialized_sumtype $for v in initialized_sumtype.variants { if initialized_sumtype is v { - $if initialized_sumtype !is $option { + $if initialized_sumtype is time.Time { + mut val := copy_type(initialized_sumtype) + decoder.decode_sumtype_time(mut val)! + return T(val) + } $else $if initialized_sumtype !is $option { mut val := copy_type(initialized_sumtype) decoder.decode_value(mut val)! return T(val) @@ -190,25 +203,30 @@ fn (decoder &Decoder) get_sumtype_type_field_node(current_node &Node[ValueInfo]) return unsafe { nil } } -fn (mut decoder Decoder) check_struct_type_valid[T](s T, current_node &Node[ValueInfo]) bool { - type_field_node := decoder.get_sumtype_type_field_node(current_node) +fn (decoder &Decoder) sumtype_type_field_matches(type_field_node &Node[ValueInfo], expected string) bool { if type_field_node == unsafe { nil } { return false } - - variant_name := typeof(s).name - if type_field_node.value.length - 2 == variant_name.len { - unsafe { - } - if unsafe { - type_field_node.value.position + 1 + variant_name.len <= decoder.json.len - && 0 == vmemcmp(decoder.json.str + type_field_node.value.position + 1, variant_name.str, variant_name.len) - } { - return true - } + if type_field_node.value.value_kind != .string { + return false + } + if type_field_node.value.length - 2 != expected.len { + return false + } + return unsafe { + type_field_node.value.position + 1 + expected.len <= decoder.json.len + && 0 == vmemcmp(decoder.json.str + type_field_node.value.position + 1, expected.str, expected.len) } +} - return false +fn (decoder &Decoder) check_sumtype_type_valid[T](value T, current_node &Node[ValueInfo]) bool { + type_field_node := decoder.get_sumtype_type_field_node(current_node) + return decoder.sumtype_type_field_matches(type_field_node, + sumtype_variant_name(typeof(value).name)) +} + +fn (mut decoder Decoder) check_struct_type_valid[T](s T, current_node &Node[ValueInfo]) bool { + return decoder.check_sumtype_type_valid(s, current_node) } fn (mut decoder Decoder) get_struct_type_workaround[T](initialized_sumtype T) bool { @@ -225,6 +243,56 @@ fn (mut decoder Decoder) get_struct_type_workaround[T](initialized_sumtype T) bo return false } +fn (mut decoder Decoder) get_time_type_workaround[T](initialized_sumtype T) bool { + $if initialized_sumtype is $sumtype || (T is $alias && T.unaliased_typ is $sumtype) { + $for v in initialized_sumtype.variants { + if initialized_sumtype is v { + $if initialized_sumtype is time.Time { + val := copy_type(initialized_sumtype) + return decoder.check_sumtype_type_valid(val, decoder.current_node) + } + } + } + } + return false +} + +fn (mut decoder Decoder) resolve_sumtype_from_type_field[T](mut val T) !bool { + if decoder.get_sumtype_type_field_node(decoder.current_node) == unsafe { nil } { + return false + } + mut has_discriminated_variant := false + $for v in val.variants { + $if v.typ is time.Time { + has_discriminated_variant = true + val = T(v) + if decoder.get_time_type_workaround(val) { + return true + } + } $else $if v.typ is $struct { + has_discriminated_variant = true + val = T(v) + if decoder.get_struct_type_workaround(val) { + return true + } + } + } + if !has_discriminated_variant { + return false + } + decoder.decode_error('could not resolve sumtype `${T.name}` from "_type" field')! + return false +} + +fn (mut decoder Decoder) decode_sumtype_time(mut val time.Time) ! { + mut wrapper := SumtypeTimeValue{ + typ: '' + value: 0 + } + decoder.decode_value(mut wrapper)! + val = time.unix(wrapper.value) +} + fn (mut decoder Decoder) init_sumtype_by_value_kind[T](mut val T, value_info ValueInfo) ! { mut failed_struct := false mut struct_variant_count := 0 @@ -295,6 +363,9 @@ fn (mut decoder Decoder) init_sumtype_by_value_kind[T](mut val T, value_info Val } } .object { + if decoder.resolve_sumtype_from_type_field(mut val)! { + return + } $for v in val.variants { $if v.typ is $map { val = T(v) diff --git a/vlib/x/json2/encode.v b/vlib/x/json2/encode.v index 1ea8bb838..b0159b95b 100644 --- a/vlib/x/json2/encode.v +++ b/vlib/x/json2/encode.v @@ -1,5 +1,7 @@ module json2 +import time + // EncoderOptions provides a list of options for encoding @[params] pub struct EncoderOptions { @@ -433,12 +435,64 @@ fn (mut encoder Encoder) encode_sumtype[T](val T) { } $else { $for variant in T.variants { if val is variant { - encoder.encode_value(val) + variant_name := sumtype_variant_name(typeof(variant.typ).name) + $if variant.typ is time.Time { + encoder.encode_sumtype_time_variant(val, variant_name) + } $else $if variant.typ is $struct { + encoder.encode_sumtype_struct_variant(val, variant_name) + } $else { + encoder.encode_value(val) + } } } } } +fn (mut encoder Encoder) encode_object_key(is_first bool, key string) bool { + if is_first { + if encoder.prettify { + encoder.increment_level() + } + } else { + encoder.output << `,` + } + if encoder.prettify { + encoder.add_indent() + } + encoder.encode_string(key) + encoder.output << `:` + if encoder.prettify { + encoder.output << ` ` + } + return false +} + +fn (mut encoder Encoder) encode_sumtype_struct_variant[T](val T, variant_name string) { + encoder.output << `{` + mut is_first := unsafe { encoder.encode_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 << `}` +} + +fn (mut encoder Encoder) encode_sumtype_time_variant(val time.Time, variant_name string) { + encoder.output << `{` + mut is_first := true + is_first = encoder.encode_object_key(is_first, '_type') + encoder.encode_string(variant_name) + is_first = encoder.encode_object_key(is_first, 'value') + encoder.encode_number(val.unix()) + if encoder.prettify && !is_first { + encoder.decrement_level() + encoder.add_indent() + } + encoder.output << `}` +} + struct EncoderFieldInfo { key_name string diff --git a/vlib/x/json2/tests/encode_struct_test.v b/vlib/x/json2/tests/encode_struct_test.v index 8273181f7..690d8805e 100644 --- a/vlib/x/json2/tests/encode_struct_test.v +++ b/vlib/x/json2/tests/encode_struct_test.v @@ -272,7 +272,7 @@ fn test_pointer() { } fn test_sumtypes() { - assert json.encode(StructType[SumTypes]{}) == '{"val":{"val":""}}' // is_none := val.$(field.name).str() == 'unknown sum type value' + assert json.encode(StructType[SumTypes]{}) == '{"val":{"val":"","_type":"${typeof(StructType[string]{}).name}"}}' // is_none := val.$(field.name).str() == 'unknown sum type value' assert json.encode(StructType[SumTypes]{ val: '' }) == '{"val":""}' assert json.encode(StructType[SumTypes]{ val: 'a' }) == '{"val":"a"}' @@ -282,7 +282,7 @@ fn test_sumtypes() { assert json.encode(StructType[SumTypes]{ val: 0 }) == '{"val":0}' assert json.encode(StructType[SumTypes]{ val: 1 }) == '{"val":1}' - assert json.encode(StructType[SumTypes]{ val: fixed_time }) == '{"val":"2022-03-11T13:54:25.000Z"}' + assert json.encode(StructType[SumTypes]{ val: fixed_time }) == '{"val":{"_type":"${typeof(time.Time{}).name.all_after_last('.')}","value":${fixed_time.unix()}}}' assert json.encode(StructType[StructType[SumTypes]]{ val: StructType[SumTypes]{ @@ -294,7 +294,7 @@ fn test_sumtypes() { val: StructType[string]{ val: '111111' } - }) == '{"val":{"val":"111111"}}' + }) == '{"val":{"val":"111111","_type":"${typeof(StructType[string]{}).name}"}}' assert json.encode(StructType[StructType[SumTypes]]{ val: StructType[SumTypes]{ diff --git a/vlib/x/json2/tests/json2_tests/json_module_compatibility_test/json_test.v b/vlib/x/json2/tests/json2_tests/json_module_compatibility_test/json_test.v index bead04260..cfd5176e7 100644 --- a/vlib/x/json2/tests/json2_tests/json_module_compatibility_test/json_test.v +++ b/vlib/x/json2/tests/json2_tests/json_module_compatibility_test/json_test.v @@ -386,7 +386,15 @@ fn test_encode_decode_sumtype() { enc := json.encode(game, enum_as_int: true) - assert enc == '{"title":"Super Mega Game","player":{"name":"Monke"},"other":[{"tag":"Pen"},{"tag":"Cookie"},1,"Stool","${t.format_rfc3339()}"]}' + assert enc == '{"title":"Super Mega Game","player":{"name":"Monke","_type":"Human"},"other":[{"tag":"Pen","_type":"Item"},{"tag":"Cookie","_type":"Item"},1,"Stool",{"_type":"${typeof(time.Time{}).name.all_after_last('.')}","value":${t.unix()}}]}' + + dec := json.decode[SomeGame](enc)! + + assert game.title == dec.title + assert game.player == dec.player + assert (game.other[2] as Animal) == .cat + assert dec.other[2] == Entity(Animal.cat) + assert (game.other[4] as time.Time).unix() == (dec.other[4] as time.Time).unix() } struct Foo3 { @@ -434,7 +442,7 @@ fn create_game_packet(data &GamePacketData) string { return json.encode(data) } -// fn test_encode_sumtype_defined_ahead() { -// ret := create_game_packet(&GamePacketData(GPScale{})) -// assert ret == '{"value":0}' -// } +fn test_encode_sumtype_defined_ahead() { + ret := create_game_packet(&GamePacketData(GPScale{})) + assert ret == '{"value":0,"_type":"GPScale"}' +} diff --git a/vlib/x/json2/tests/json_module_compatibility_test/json_test.v b/vlib/x/json2/tests/json_module_compatibility_test/json_test.v index 798e364d3..1fe66d230 100644 --- a/vlib/x/json2/tests/json_module_compatibility_test/json_test.v +++ b/vlib/x/json2/tests/json_module_compatibility_test/json_test.v @@ -389,7 +389,15 @@ fn test_encode_decode_sumtype() { enc := json.encode(game, enum_as_int: true) - assert enc == '{"title":"Super Mega Game","player":{"name":"Monke"},"other":[{"tag":"Pen"},{"tag":"Cookie"},1,"Stool","${t.format_rfc3339()}"]}' + assert enc == '{"title":"Super Mega Game","player":{"name":"Monke","_type":"Human"},"other":[{"tag":"Pen","_type":"Item"},{"tag":"Cookie","_type":"Item"},1,"Stool",{"_type":"${typeof(time.Time{}).name.all_after_last('.')}","value":${t.unix()}}]}' + + dec := json.decode[SomeGame](enc)! + + assert game.title == dec.title + assert game.player == dec.player + assert (game.other[2] as Animal) == .cat + assert dec.other[2] == Entity(Animal.cat) + assert (game.other[4] as time.Time).unix() == (dec.other[4] as time.Time).unix() } struct Foo3 { @@ -437,7 +445,7 @@ fn create_game_packet(data &GamePacketData) string { return json.encode(data) } -// fn test_encode_sumtype_defined_ahead() { -// ret := create_game_packet(&GamePacketData(GPScale{})) -// assert ret == '{"value":0}' -// } +fn test_encode_sumtype_defined_ahead() { + ret := create_game_packet(&GamePacketData(GPScale{})) + assert ret == '{"value":0,"_type":"GPScale"}' +} -- 2.39.5