From 27ea3955e8b2a3813338a8788c00fff9db28dfbc Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Mon, 25 May 2026 14:32:21 +0300 Subject: [PATCH] x.json2: fix decode/encode regressions (fix #26967) (#27252) --- vlib/x/json2/decode.v | 64 +++++++++++++++-- vlib/x/json2/decode_sumtype.v | 3 +- vlib/x/json2/encode.v | 70 +++++++++++++++---- .../json2_test_new_err/1_json_decode_test.vv | 21 ++++++ .../tests/json2_test_new_err/1_json_test.vv | 27 +++++++ .../json2_test_new_err/2_json_decode_test.vv | 15 ++++ .../tests/json2_test_new_err/2_json_test.vv | 48 +++++++++++++ .../json2_test_new_err/3_json_decode_test.vv | 15 ++++ .../tests/json2_test_new_err/3_json_test.v | 21 ++++++ .../json2_test_new_err/4_json_decode_test.vv | 20 ++++++ .../tests/json2_test_new_err/4_json_test.vv | 57 +++++++++++++++ .../json2_test_new_err/5_json_decode_test.vv | 16 +++++ .../json_decode_sumtype_option_test.vv | 8 +++ .../json_decode_with_sumtype_test.v | 21 ++++++ .../json_encode_primite_test.vv | 15 ++++ .../json_generic_array_test.vv | 46 ++++++++++++ .../json_option_alias_test.v | 41 +++++++++++ .../json_prim_type_validation_test.vv | 40 +++++++++++ .../tests/json2_test_new_err/json_raw_test.vv | 15 ++++ .../json2_test_new_err/json_sumtype_test.v | 35 ++++++++++ 20 files changed, 578 insertions(+), 20 deletions(-) create mode 100644 vlib/x/json2/tests/json2_test_new_err/1_json_decode_test.vv create mode 100644 vlib/x/json2/tests/json2_test_new_err/1_json_test.vv create mode 100644 vlib/x/json2/tests/json2_test_new_err/2_json_decode_test.vv create mode 100644 vlib/x/json2/tests/json2_test_new_err/2_json_test.vv create mode 100644 vlib/x/json2/tests/json2_test_new_err/3_json_decode_test.vv create mode 100644 vlib/x/json2/tests/json2_test_new_err/3_json_test.v create mode 100644 vlib/x/json2/tests/json2_test_new_err/4_json_decode_test.vv create mode 100644 vlib/x/json2/tests/json2_test_new_err/4_json_test.vv create mode 100644 vlib/x/json2/tests/json2_test_new_err/5_json_decode_test.vv create mode 100644 vlib/x/json2/tests/json2_test_new_err/json_decode_sumtype_option_test.vv create mode 100644 vlib/x/json2/tests/json2_test_new_err/json_decode_with_sumtype_test.v create mode 100644 vlib/x/json2/tests/json2_test_new_err/json_encode_primite_test.vv create mode 100644 vlib/x/json2/tests/json2_test_new_err/json_generic_array_test.vv create mode 100644 vlib/x/json2/tests/json2_test_new_err/json_option_alias_test.v create mode 100644 vlib/x/json2/tests/json2_test_new_err/json_prim_type_validation_test.vv create mode 100644 vlib/x/json2/tests/json2_test_new_err/json_raw_test.vv create mode 100644 vlib/x/json2/tests/json2_test_new_err/json_sumtype_test.v diff --git a/vlib/x/json2/decode.v b/vlib/x/json2/decode.v index dd38fd056..70913febf 100644 --- a/vlib/x/json2/decode.v +++ b/vlib/x/json2/decode.v @@ -566,10 +566,34 @@ fn decode_struct_key[T](mut decoder Decoder, val T, key_info ValueInfo, prefix s } } } $else $if field.unaliased_typ is $array_dynamic { - new_val.$(field.name).clear() - decoder.decode_array(mut new_val.$(field.name))! + if decoder.current_node.value.value_kind == .null + && !field_info.is_required { + new_val.$(field.name).clear() + decoder.skip_current_value() + } else { + new_val.$(field.name).clear() + decoder.decode_array(mut new_val.$(field.name))! + } } $else $if field.unaliased_typ is $map { - decoder.decode_map(mut new_val.$(field.name))! + if decoder.current_node.value.value_kind == .null + && !field_info.is_required { + new_val.$(field.name).clear() + decoder.skip_current_value() + } else { + decoder.decode_map(mut new_val.$(field.name))! + } + } $else $if field.unaliased_typ is string { + value_info := decoder.current_node.value + if value_info.value_kind == .object || value_info.value_kind == .array { + new_val.$(field.name) = decoder.json[value_info.position..value_info.position + + value_info.length] + decoder.skip_current_value() + } else if value_info.value_kind == .null && !field_info.is_required { + new_val.$(field.name) = '' + decoder.skip_current_value() + } else { + decoder.decode_value(mut new_val.$(field.name))! + } } $else $if field.indirections == 1 { if decoder.current_node.value.value_kind == .null { new_val.$(field.name) = unsafe { nil } @@ -917,10 +941,38 @@ fn (mut decoder Decoder) decode_value[T](mut val T) ! { } } } $else $if field.unaliased_typ is $array_dynamic { - val.$(field.name).clear() - decoder.decode_array(mut val.$(field.name))! + if decoder.current_node.value.value_kind == .null + && !field_info.is_required { + val.$(field.name).clear() + decoder.skip_current_value() + } else { + val.$(field.name).clear() + decoder.decode_array(mut val.$(field.name))! + } } $else $if field.unaliased_typ is $map { - decoder.decode_map(mut val.$(field.name))! + if decoder.current_node.value.value_kind == .null + && !field_info.is_required { + val.$(field.name).clear() + decoder.skip_current_value() + } else { + decoder.decode_map(mut val.$(field.name))! + } + } $else $if field.unaliased_typ is string { + value_info := decoder.current_node.value + if value_info.value_kind == .object + || value_info.value_kind == .array { + val.$(field.name) = decoder.json[value_info.position..value_info.position + + value_info.length] + decoder.skip_current_value() + } else if value_info.value_kind == .null + && !field_info.is_required { + val.$(field.name) = '' + decoder.skip_current_value() + } else { + mut decoded_field_value := val.$(field.name) + decoder.decode_value(mut decoded_field_value)! + val.$(field.name) = decoded_field_value + } } $else $if field.indirections == 1 { if decoder.current_node.value.value_kind == .null { val.$(field.name) = unsafe { nil } diff --git a/vlib/x/json2/decode_sumtype.v b/vlib/x/json2/decode_sumtype.v index 419e7717d..842428aa0 100644 --- a/vlib/x/json2/decode_sumtype.v +++ b/vlib/x/json2/decode_sumtype.v @@ -116,7 +116,8 @@ fn get_array_element_type[T](_arr []T) T { } fn (mut decoder Decoder) check_array_type_valid[T](arr []T, current_node &Node[ValueInfo]) bool { - return decoder.check_element_type_valid(get_array_element_type(arr), current_node) + element := get_array_element_type(arr) + return decoder.check_element_type_valid(element, current_node) } fn (mut decoder Decoder) get_array_type_workaround[T](initialized_sumtype T) bool { diff --git a/vlib/x/json2/encode.v b/vlib/x/json2/encode.v index 6661b7086..ceb8ecb30 100644 --- a/vlib/x/json2/encode.v +++ b/vlib/x/json2/encode.v @@ -463,6 +463,14 @@ fn (mut encoder Encoder) encode_sumtype[T](val T) { } } $else $if variant.typ is $map { encoder.encode_value(val) + } $else $if variant.typ is $array_dynamic { + if T.name in ['x.json2.Any', 'json2.Any', 'Any'] { + variant_value := val + encoder.encode_value(variant_value) + } else { + variant_value := val + encoder.encode_array_of_sumtype_variants(variant_value) + } } $else { variant_value := val encoder.encode_value(variant_value) @@ -509,6 +517,40 @@ fn (mut encoder Encoder) encode_sumtype_struct_variant[T](val T, variant_name st encoder.output << `}` } +fn (mut encoder Encoder) encode_array_of_sumtype_variants[T](val []T) { + encoder.output << `[` + if encoder.prettify { + encoder.increment_level() + encoder.add_indent() + } + for i, item in val { + $if T is time.Time { + encoder.encode_value(item) + } $else $if T is JsonEncoder { + encoder.encode_value(item) + } $else $if T is Encodable { + encoder.encode_value(item) + } $else $if T is $struct { + elem_name := sumtype_variant_name(T.name) + encoder.encode_sumtype_struct_variant(item, elem_name) + } $else { + encoder.encode_value(item) + } + if i < val.len - 1 { + encoder.output << `,` + if encoder.prettify { + encoder.add_indent() + } + } else { + if encoder.prettify { + encoder.decrement_level() + encoder.add_indent() + } + } + } + encoder.output << `]` +} + @[markused] fn (mut encoder Encoder) encode_sumtype_time_variant(val time.Time, variant_name string) { encoder.output << `{` @@ -538,19 +580,7 @@ fn get_value_from_optional[T](val ?T) T { } fn check_not_empty[T](val T) ?bool { - $if T.indirections != 0 { - return val != unsafe { nil } - } $else $if T.unaliased_typ is string { - if val == '' { - return false - } - } $else $if T.unaliased_typ is $int || T.unaliased_typ is $float { - if val == 0 { - return false - } - } $else $if T.unaliased_typ is $array || T.unaliased_typ is $map { - return val.len != 0 - } $else $if val is ?string { + $if val is ?string { opt := ?string(val) if sval := opt { return sval != '' @@ -574,6 +604,20 @@ fn check_not_empty[T](val T) ?bool { return fval != 0.0 } return false + } $else $if T is $option { + return !struct_field_is_none(val) + } $else $if T.indirections != 0 { + return val != unsafe { nil } + } $else $if T.unaliased_typ is string { + if val == '' { + return false + } + } $else $if T.unaliased_typ is $int || T.unaliased_typ is $float { + if val == 0 { + return false + } + } $else $if T.unaliased_typ is $array || T.unaliased_typ is $map { + return val.len != 0 } return true } diff --git a/vlib/x/json2/tests/json2_test_new_err/1_json_decode_test.vv b/vlib/x/json2/tests/json2_test_new_err/1_json_decode_test.vv new file mode 100644 index 000000000..9ca9efd4f --- /dev/null +++ b/vlib/x/json2/tests/json2_test_new_err/1_json_decode_test.vv @@ -0,0 +1,21 @@ +import x.json2 as json + +struct TestTwin { + id int + seed string + pubkey string +} + +struct TestTwins { +mut: + twins []TestTwin @[required] +} + +fn test_json_decode_fails_to_decode_unrecognised_array_of_dicts() { + data := '[{"twins":[{"id":123,"seed":"abcde","pubkey":"xyzasd"},{"id":456,"seed":"dfgdfgdfgd","pubkey":"skjldskljh45sdf"}]}]' + json.decode[TestTwins](data) or { + assert err.msg() == "expected field 'twins' is missing" + return + } + assert false +} diff --git a/vlib/x/json2/tests/json2_test_new_err/1_json_test.vv b/vlib/x/json2/tests/json2_test_new_err/1_json_test.vv new file mode 100644 index 000000000..7fbdb99d9 --- /dev/null +++ b/vlib/x/json2/tests/json2_test_new_err/1_json_test.vv @@ -0,0 +1,27 @@ +import x.json2 as json +import time + +struct User2 { + age int + nums []int + reg_date time.Time +} + +struct User { + age int + nums []int + last_name string @[json: lastName] + is_registered bool @[json: IsRegistered] + typ int @[json: 'type'] + pets string @[json: 'pet_animals'; raw] +} + +fn test_encode_decode_time() { + user := User2{ + age: 25 + reg_date: time.new(year: 2020, month: 12, day: 22, hour: 7, minute: 23) + } + s := json.encode(user) + println(s) + assert s.contains('"reg_date":1608621780') +} diff --git a/vlib/x/json2/tests/json2_test_new_err/2_json_decode_test.vv b/vlib/x/json2/tests/json2_test_new_err/2_json_decode_test.vv new file mode 100644 index 000000000..2a11ceed6 --- /dev/null +++ b/vlib/x/json2/tests/json2_test_new_err/2_json_decode_test.vv @@ -0,0 +1,15 @@ +import x.json2 as json + +struct DbConfig { + host string + dbname string + user string +} + +fn test_decode_error_message_should_have_enough_context_empty() { + json.decode[DbConfig]('') or { + assert err.msg() == 'failed to decode JSON string' + return + } + assert false +} diff --git a/vlib/x/json2/tests/json2_test_new_err/2_json_test.vv b/vlib/x/json2/tests/json2_test_new_err/2_json_test.vv new file mode 100644 index 000000000..13961e6f9 --- /dev/null +++ b/vlib/x/json2/tests/json2_test_new_err/2_json_test.vv @@ -0,0 +1,48 @@ +import x.json2 as json + +struct City { + name string +} + +struct Country { + cities []City + name string +} + +struct Data { + countries []Country + users map[string]User + extra map[string]map[string]int +} + +struct User { + age int + nums []int + last_name string @[json: lastName] + is_registered bool @[json: IsRegistered] + typ int @[json: 'type'] + pets string @[json: 'pet_animals'; raw] +} + +fn test_errors() { + invalid_array := fn () { + data := '{"countries":[{"cities":[{"name":"London"},{"name":"Manchester"}],"name":"UK"},{"cities":{"name":"Donlon"},"name":"KU"}],"users":{"Foo":{"age":10,"nums":[1,2,3],"lastName":"Johnson","IsRegistered":true,"type":0,"pet_animals":"little foo"},"Boo":{"age":20,"nums":[5,3,1],"lastName":"Smith","IsRegistered":false,"type":4,"pet_animals":"little boo"}},"extra":{"2":{"n1":2,"n2":4,"n3":8,"n4":16},"3":{"n1":3,"n2":9,"n3":27,"n4":81}}}' + json.decode[Data](data) or { + println(err) + assert err.msg().starts_with('Json element is not an array:') + return + } + assert false + } + invalid_object := fn () { + data := '{"countries":[{"cities":[{"name":"London"},{"name":"Manchester"}],"name":"UK"},{"cities":[{"name":"Donlon"},{"name":"Termanches"}],"name":"KU"}],"users":[{"age":10,"nums":[1,2,3],"lastName":"Johnson","IsRegistered":true,"type":0,"pet_animals":"little foo"},{"age":20,"nums":[5,3,1],"lastName":"Smith","IsRegistered":false,"type":4,"pet_animals":"little boo"}],"extra":{"2":{"n1":2,"n2":4,"n3":8,"n4":16},"3":{"n1":3,"n2":9,"n3":27,"n4":81}}}' + json.decode[Data](data) or { + println(err) + assert err.msg().starts_with('Json element is not an object:') + return + } + assert false + } + invalid_array() + invalid_object() +} diff --git a/vlib/x/json2/tests/json2_test_new_err/3_json_decode_test.vv b/vlib/x/json2/tests/json2_test_new_err/3_json_decode_test.vv new file mode 100644 index 000000000..5dc4052a5 --- /dev/null +++ b/vlib/x/json2/tests/json2_test_new_err/3_json_decode_test.vv @@ -0,0 +1,15 @@ +import x.json2 as json + +struct DbConfig { + host string + dbname string + user string +} + +fn test_decode_error_message_should_have_enough_context_just_brace() { + json.decode[DbConfig]('{') or { + assert err.msg() == 'failed to decode JSON string: {' + return + } + assert false +} diff --git a/vlib/x/json2/tests/json2_test_new_err/3_json_test.v b/vlib/x/json2/tests/json2_test_new_err/3_json_test.v new file mode 100644 index 000000000..257d777c8 --- /dev/null +++ b/vlib/x/json2/tests/json2_test_new_err/3_json_test.v @@ -0,0 +1,21 @@ +import x.json2 as json + +struct Info { + id int + items []string + maps map[string]string +} + +fn test_decode_null_object() ! { + info := json.decode[Info]('{"id": 22, "items": null, "maps": null}')! + assert info.id == 22 + assert '${info.items}' == '[]' + assert '${info.maps}' == '{}' +} + +fn test_decode_missing_maps_field() ! { + info := json.decode[Info]('{"id": 22, "items": null}')! + assert info.id == 22 + assert '${info.items}' == '[]' + assert '${info.maps}' == '{}' +} diff --git a/vlib/x/json2/tests/json2_test_new_err/4_json_decode_test.vv b/vlib/x/json2/tests/json2_test_new_err/4_json_decode_test.vv new file mode 100644 index 000000000..5ea624374 --- /dev/null +++ b/vlib/x/json2/tests/json2_test_new_err/4_json_decode_test.vv @@ -0,0 +1,20 @@ +import x.json2 as json + +struct DbConfig { + host string + dbname string + user string +} + +fn test_decode_error_message_should_have_enough_context_trailing_comma_at_end() { + txt := '{ + "host": "localhost", + "dbname": "alex", + "user": "alex", +}' + json.decode[DbConfig](txt) or { + assert err.msg().contains(' "user": "alex",\n}') + return + } + assert false +} diff --git a/vlib/x/json2/tests/json2_test_new_err/4_json_test.vv b/vlib/x/json2/tests/json2_test_new_err/4_json_test.vv new file mode 100644 index 000000000..9380fc1be --- /dev/null +++ b/vlib/x/json2/tests/json2_test_new_err/4_json_test.vv @@ -0,0 +1,57 @@ +import x.json2 as json + +struct ChildNullish { + name string +} + +struct NullishStruct { + name string + lastname string + age int + salary f32 + child ChildNullish +} + +struct RequiredStruct { + name string @[required] + lastname string +} + +fn test_required() ! { + nullish_one := + json.decode[NullishStruct]('{"name":"Peter", "lastname":null, "age":28,"salary":95000.5,"title":"worker"}')! + assert nullish_one.name == 'Peter' + assert nullish_one.lastname == '' + assert nullish_one.age == 28 + assert nullish_one.salary == 95000.5 + assert nullish_one.child.name == '' + + nullish_two := + json.decode[NullishStruct]('{"name":"Peter", "lastname": "Parker", "age":28,"salary":95000.5,"title":"worker"}')! + assert nullish_two.name == 'Peter' + assert nullish_two.lastname == 'Parker' + assert nullish_two.age == 28 + assert nullish_two.salary == 95000.5 + assert nullish_two.child.name == '' + + nullish_third := + json.decode[NullishStruct]('{"name":"Peter", "age":28,"salary":95000.5,"title":"worker", "child": {"name":"Nullish"}}')! + assert nullish_third.name == 'Peter' + assert nullish_third.lastname == '' + assert nullish_third.age == 28 + assert nullish_third.salary == 95000.5 + assert nullish_third.child.name == 'Nullish' + + required_struct := json.decode[RequiredStruct]('{"name":"Peter", "lastname": "Parker"}')! + assert required_struct.name == 'Peter' + assert required_struct.lastname == 'Parker' + + required_struct_err := json.decode[RequiredStruct]('{"name": null, "lastname": "Parker"}') or { + assert err.msg() == "type mismatch for field 'name', expecting `string` type, got: null" + RequiredStruct{ + name: 'Peter' + lastname: 'Parker' + } + } + println(required_struct_err) +} diff --git a/vlib/x/json2/tests/json2_test_new_err/5_json_decode_test.vv b/vlib/x/json2/tests/json2_test_new_err/5_json_decode_test.vv new file mode 100644 index 000000000..ebc6b5391 --- /dev/null +++ b/vlib/x/json2/tests/json2_test_new_err/5_json_decode_test.vv @@ -0,0 +1,16 @@ +import x.json2 as json + +struct DbConfig { + host string + dbname string + user string +} + +fn test_decode_error_message_should_have_enough_context_in_the_middle() { + txt := '{"host": "localhost", "dbname": "alex" "user": "alex", "port": "1234"}' + json.decode[DbConfig](txt) or { + assert err.msg().contains('ost", "dbname": "alex" "user":') + return + } + assert false +} diff --git a/vlib/x/json2/tests/json2_test_new_err/json_decode_sumtype_option_test.vv b/vlib/x/json2/tests/json2_test_new_err/json_decode_sumtype_option_test.vv new file mode 100644 index 000000000..46a3c743e --- /dev/null +++ b/vlib/x/json2/tests/json2_test_new_err/json_decode_sumtype_option_test.vv @@ -0,0 +1,8 @@ +import x.json2 as json + +type Any = string | f32 | bool | ?int + +fn test_main() { + x := json.decode[[]Any]('["hi", -9.8e7, true, null]')! + assert dump(x) == [Any('hi'), Any(f32(-9.8e+7)), Any(true), Any('')] +} diff --git a/vlib/x/json2/tests/json2_test_new_err/json_decode_with_sumtype_test.v b/vlib/x/json2/tests/json2_test_new_err/json_decode_with_sumtype_test.v new file mode 100644 index 000000000..10f260624 --- /dev/null +++ b/vlib/x/json2/tests/json2_test_new_err/json_decode_with_sumtype_test.v @@ -0,0 +1,21 @@ +import x.json2 as json + +type Test = []bool | []int | []string | string + +struct Some { + t Test +} + +fn test_json_decode_with_sumtype() { + v1 := json.decode[Some]('{"t": ["string", "string2"]}')! + println(v1) + assert v1.t == Test(['string', 'string2']) + + v2 := json.decode[Some]('{"t": [11, 22]}')! + println(v2) + assert v2.t == Test([11, 22]) + + v3 := json.decode[Some]('{"t": [true, false]}')! + println(v3) + assert v3.t == Test([true, false]) +} diff --git a/vlib/x/json2/tests/json2_test_new_err/json_encode_primite_test.vv b/vlib/x/json2/tests/json2_test_new_err/json_encode_primite_test.vv new file mode 100644 index 000000000..2b66fc67a --- /dev/null +++ b/vlib/x/json2/tests/json2_test_new_err/json_encode_primite_test.vv @@ -0,0 +1,15 @@ +import x.json2 as json + +struct UnicodeString { + emoji string +} + +fn test_encode_unicode_as_ascii_escape_sequences() { + valid_json := r'{"emoji":"\u3007"}' + decoded := json.decode[UnicodeString](valid_json)! + assert decoded.emoji == '〇' + assert json.encode(UnicodeString{ + emoji: '〇' + }) == valid_json + assert json.encode('😀') == r'"\uD83D\ude00"' +} diff --git a/vlib/x/json2/tests/json2_test_new_err/json_generic_array_test.vv b/vlib/x/json2/tests/json2_test_new_err/json_generic_array_test.vv new file mode 100644 index 000000000..4e400c4dc --- /dev/null +++ b/vlib/x/json2/tests/json2_test_new_err/json_generic_array_test.vv @@ -0,0 +1,46 @@ +import x.json2 as json + +pub struct DocumentFindFilter[T] { +pub: + selector ?T + selector2 ?[]T + selector3 ?[1]T + limit ?int + skip ?int + fields ?[]string // This breaks the compiler when encoding to JSON + conflicts ?bool + read_quorum ?int @[json: r] + update ?bool + stable ?bool + stale ?string + execution_stats ?bool +} + +fn test_string() { + t := DocumentFindFilter[string]{ + selector: 'aa' + selector2: ['a', 'b'] + selector3: ['z']! + } + + assert json.encode(t) == '{"selector":"aa","selector2":["a","b"],"selector3":["z"]}' + assert json.decode[DocumentFindFilter[string]]('{"selector":"aa","selector2":["a","b"],"selector3":["z"]}')! == t +} + +fn test_int() ! { + t := DocumentFindFilter[int]{ + selector: 1 + selector2: [1, 2] + selector3: [3]! + } + + assert json.encode(t) == '{"selector":1,"selector2":[1,2],"selector3":[3]}' + assert json.decode[DocumentFindFilter[int]]('{"selector":1,"selector2":[1,2],"selector3":[3]}')! == t +} + +fn test_none() ! { + t := DocumentFindFilter[int]{} + + assert json.encode(t) == '{}' + assert json.decode[DocumentFindFilter[int]]('{}')! == t +} diff --git a/vlib/x/json2/tests/json2_test_new_err/json_option_alias_test.v b/vlib/x/json2/tests/json2_test_new_err/json_option_alias_test.v new file mode 100644 index 000000000..6cf550315 --- /dev/null +++ b/vlib/x/json2/tests/json2_test_new_err/json_option_alias_test.v @@ -0,0 +1,41 @@ +import x.json2 as json + +struct Test { + optional_alias ?MyAlias // primitive + optional_struct ?MyAlias2 // complex +} + +struct Complex { + a int = 3 +} + +type MyAlias = int +type MyAlias2 = Complex + +fn test_empty() { + test := Test{} + encoded := json.encode(test) + assert dump(encoded) == '{}' + assert json.decode[Test]('{}')! == test +} + +fn test_value() { + test := Test{ + optional_alias: 1 + } + encoded := json.encode(test) + assert dump(encoded) == '{"optional_alias":1}' + assert json.decode[Test]('{"optional_alias":1}')! == test +} + +fn test_value_2() { + test := Test{ + optional_alias: 1 + optional_struct: Complex{ + a: 1 + } + } + encoded := json.encode(test) + assert dump(encoded) == '{"optional_alias":1,"optional_struct":{"a":1}}' + assert json.decode[Test]('{"optional_alias":1,"optional_struct":{"a":1}}')! == test +} diff --git a/vlib/x/json2/tests/json2_test_new_err/json_prim_type_validation_test.vv b/vlib/x/json2/tests/json2_test_new_err/json_prim_type_validation_test.vv new file mode 100644 index 000000000..8699f68ac --- /dev/null +++ b/vlib/x/json2/tests/json2_test_new_err/json_prim_type_validation_test.vv @@ -0,0 +1,40 @@ +module main + +import x.json2 as json + +struct MyStruct { + name string // should fail + age ?int + active bool +} + +struct TestStructOne { + property_one string @[json: 'propertyOne'] + property_two string @[json: 'propertyTwo'] +} + +fn test_main() { + mut errors := 0 + json.decode[MyStruct]('{ "name": 1}') or { + errors++ + assert err.msg() == "type mismatch for field 'name', expecting `string` type, got: 1" + } + json.decode[MyStruct]('{ "name": "John Doe", "age": ""}') or { + errors++ + assert err.msg() == 'type mismatch for field \'age\', expecting `?int` type, got: ""' + } + json.decode[MyStruct]('{ "name": "John Doe", "age": 1, "active": ""}') or { + errors++ + assert err.msg() == 'type mismatch for field \'active\', expecting `bool` type, got: ""' + } + res := json.decode[MyStruct]('{ "name": "John Doe", "age": "1"}') or { panic(err) } + assert errors == 3 + assert res.name == 'John Doe' +} + +fn test_decode_object_into_string_field() { + payload := '{"propertyOne":"property_two should stay a regular string {}","propertyTwo":{}}' + res := json.decode[TestStructOne](payload) or { panic(err) } + assert res.property_one == 'property_two should stay a regular string {}' + assert res.property_two == '{}' +} diff --git a/vlib/x/json2/tests/json2_test_new_err/json_raw_test.vv b/vlib/x/json2/tests/json2_test_new_err/json_raw_test.vv new file mode 100644 index 000000000..04331326a --- /dev/null +++ b/vlib/x/json2/tests/json2_test_new_err/json_raw_test.vv @@ -0,0 +1,15 @@ +import x.json2 as json + +struct TestRawStringifiedObject { + metadata string @[raw] +} + +fn test_stringified_object_returns_error_for_raw_field() { + stringified_json := + json.encode('{"metadata":{"topLevelProperty":{"nestedProperty1":"Value 1"}}}') + json.decode[TestRawStringifiedObject](stringified_json) or { + assert err.msg().starts_with('Json element is not an object:') + return + } + assert false +} diff --git a/vlib/x/json2/tests/json2_test_new_err/json_sumtype_test.v b/vlib/x/json2/tests/json2_test_new_err/json_sumtype_test.v new file mode 100644 index 000000000..a9802ef38 --- /dev/null +++ b/vlib/x/json2/tests/json2_test_new_err/json_sumtype_test.v @@ -0,0 +1,35 @@ +import x.json2 as json + +type Prices = Price | []Price + +pub struct ShopResponseData { + attributes Attributes +} + +struct Attributes { + price ?Prices +} + +struct Price { + net f64 +} + +fn test_main() { + data3 := json.encode(ShopResponseData{ + attributes: Attributes{ + price: Prices([Price{ + net: 1.2 + }]) + } + }) + assert data3 == '{"attributes":{"price":[{"net":1.2,"_type":"Price"}]}}' + + entity3 := json.decode[ShopResponseData](data3) or { panic(err) } + assert entity3 == ShopResponseData{ + attributes: Attributes{ + price: Prices([Price{ + net: 1.2 + }]) + } + } +} -- 2.39.5