diff --git a/doc/docs.md b/doc/docs.md index bae2078658..a1d6d9fef2 100644 --- a/doc/docs.md +++ b/doc/docs.md @@ -3450,7 +3450,12 @@ struct Foo { } struct User { - name string + // Adding a [required] attribute will make decoding fail, if that + // field is not present in the input. + // If a field is not [required], but is missing, it will be assumed + // to have its default value, like 0 for numbers, or '' for strings, + // and decoding will not fail. + name string [required] age int // Use the `skip` attribute to skip certain fields foo Foo [skip] @@ -3460,7 +3465,7 @@ struct User { data := '{ "name": "Frodo", "lastName": "Baggins", "age": 25 }' user := json.decode(User, data) or { - eprintln('Failed to decode json') + eprintln('Failed to decode json, error: $err') return } println(user.name) diff --git a/vlib/json/json_decode_test.v b/vlib/json/json_decode_test.v new file mode 100644 index 0000000000..2c4448fcae --- /dev/null +++ b/vlib/json/json_decode_test.v @@ -0,0 +1,35 @@ +import 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 +} + +fn test_json_decode_works_with_a_dict_of_arrays() { + data := '{"twins":[{"id":123,"seed":"abcde","pubkey":"xyzasd"},{"id":456,"seed":"dfgdfgdfgd","pubkey":"skjldskljh45sdf"}]}' + res := json.decode(TestTwins, data) or { + assert false + exit(1) + } + assert res.twins[0].id == 123 + assert res.twins[0].seed == 'abcde' + assert res.twins[0].pubkey == 'xyzasd' + assert res.twins[1].id == 456 + assert res.twins[1].seed == 'dfgdfgdfgd' + assert res.twins[1].pubkey == 'skjldskljh45sdf' +} diff --git a/vlib/json/json_test.v b/vlib/json/json_test.v index ec32016029..8191462eeb 100644 --- a/vlib/json/json_test.v +++ b/vlib/json/json_test.v @@ -14,15 +14,12 @@ struct Employee { title JobTitle } -fn test_simple() { +fn test_simple() ? { x := Employee{'Peter', 28, 95000.5, .worker} s := json.encode(x) eprintln('Employee x: $s') assert s == '{"name":"Peter","age":28,"salary":95000.5,"title":2}' - y := json.decode(Employee, s) or { - assert false - Employee{} - } + y := json.decode(Employee, s) ? eprintln('Employee y: $y') assert y.name == 'Peter' assert y.age == 28 @@ -59,11 +56,11 @@ struct User { pets string [json: 'pet_animals'; raw] } -fn test_parse_user() { +fn test_parse_user() ? { s := '{"age": 10, "nums": [1,2,3], "type": 1, "lastName": "Johnson", "IsRegistered": true, "pet_animals": {"name": "Bob", "animal": "Dog"}}' - u2 := json.decode(User2, s) or { exit(1) } + u2 := json.decode(User2, s) ? println(u2) - u := json.decode(User, s) or { exit(1) } + u := json.decode(User, s) ? println(u) assert u.age == 10 assert u.last_name == 'Johnson' @@ -76,7 +73,7 @@ fn test_parse_user() { assert u.pets == '{"name":"Bob","animal":"Dog"}' } -fn test_encode_decode_time() { +fn test_encode_decode_time() ? { user := User2{ age: 25 reg_date: time.new_time(year: 2020, month: 12, day: 22, hour: 7, minute: 23) @@ -84,10 +81,7 @@ fn test_encode_decode_time() { s := json.encode(user) println(s) assert s.contains('"reg_date":1608621780') - user2 := json.decode(User2, s) or { - assert false - return - } + user2 := json.decode(User2, s) ? assert user2.reg_date.str() == '2020-12-22 07:23:00' println(user2) println(user2.reg_date) @@ -146,11 +140,8 @@ struct Country { name string } -fn test_struct_in_struct() { - country := json.decode(Country, '{ "name": "UK", "cities": [{"name":"London"}, {"name":"Manchester"}]}') or { - assert false - exit(1) - } +fn test_struct_in_struct() ? { + country := json.decode(Country, '{ "name": "UK", "cities": [{"name":"London"}, {"name":"Manchester"}]}') ? assert country.name == 'UK' assert country.cities.len == 2 assert country.cities[0].name == 'London' @@ -171,20 +162,14 @@ fn test_encode_map() { assert out == expected } -fn test_parse_map() { +fn test_parse_map() ? { expected := map{ 'one': 1 'two': 2 'three': 3 'four': 4 } - out := json.decode(map[string]int, '{"one":1,"two":2,"three":3,"four":4}') or { - assert false - r := map{ - '': 0 - } - r - } + out := json.decode(map[string]int, '{"one":1,"two":2,"three":3,"four":4}') ? println(out) assert out == expected } @@ -195,7 +180,7 @@ struct Data { extra map[string]map[string]int } -fn test_nested_type() { +fn test_nested_type() ? { data_expected := '{"countries":[{"cities":[{"name":"London"},{"name":"Manchester"}],"name":"UK"},{"cities":[{"name":"Donlon"},{"name":"Termanches"}],"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}}}' data := Data{ countries: [ @@ -244,10 +229,7 @@ fn test_nested_type() { out := json.encode(data) println(out) assert out == data_expected - data2 := json.decode(Data, data_expected) or { - assert false - Data{} - } + data2 := json.decode(Data, data_expected) ? assert data2.countries.len == data.countries.len for i in 0 .. 1 { assert data2.countries[i].name == data.countries[i].name @@ -277,11 +259,11 @@ pub: data T } -fn test_generic_struct() { +fn test_generic_struct() ? { foo_int := Foo{'bar', 12} foo_enc := json.encode(foo_int) assert foo_enc == '{"name":"bar","data":12}' - foo_dec := json.decode(Foo, foo_enc) or { exit(1) } + foo_dec := json.decode(Foo, foo_enc) ? assert foo_dec.name == 'bar' assert foo_dec.data == 12 } @@ -315,11 +297,8 @@ struct Message { id ID } -fn test_decode_alias_struct() { - msg := json.decode(Message, '{"id": "118499178790780929"}') or { - assert false - Message{} - } +fn test_decode_alias_struct() ? { + msg := json.decode(Message, '{"id": "118499178790780929"}') ? // hacky way of comparing aliased strings assert msg.id.str() == '118499178790780929' } @@ -336,29 +315,20 @@ struct List { items []string } -fn test_list() { - list := json.decode(List, '{"id": 1, "items": ["1", "2"]}') or { - println('error') - return - } +fn test_list() ? { + list := json.decode(List, '{"id": 1, "items": ["1", "2"]}') ? assert list.id == 1 assert list.items == ['1', '2'] } -fn test_list_no_id() { - list := json.decode(List, '{"items": ["1", "2"]}') or { - println('error') - return - } +fn test_list_no_id() ? { + list := json.decode(List, '{"items": ["1", "2"]}') ? assert list.id == 0 assert list.items == ['1', '2'] } -fn test_list_no_items() { - list := json.decode(List, '{"id": 1}') or { - println('error') - return - } +fn test_list_no_items() ? { + list := json.decode(List, '{"id": 1}') ? assert list.id == 1 assert list.items == [] } @@ -369,8 +339,8 @@ struct Info { maps map[string]string } -fn test_decode_null_object() { - info := json.decode(Info, '{"id": 22, "items": null, "maps": null}') or { panic(err) } +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' == '{}' diff --git a/vlib/v/gen/c/json.v b/vlib/v/gen/c/json.v index 5926da18da..cfc480dea5 100644 --- a/vlib/v/gen/c/json.v +++ b/vlib/v/gen/c/json.v @@ -113,20 +113,34 @@ $enc_fn_dec { fn (mut g Gen) gen_struct_enc_dec(type_info ast.TypeInfo, styp string, mut enc strings.Builder, mut dec strings.Builder) { info := type_info as ast.Struct for field in info.fields { - if field.attrs.contains('skip') { - continue - } mut name := field.name + mut is_raw := false + mut is_skip := false + mut is_required := false for attr in field.attrs { - if attr.name == 'json' { - name = attr.arg - break + match attr.name { + 'json' { + name = attr.arg + } + 'skip' { + is_skip = true + } + 'raw' { + is_raw = true + } + 'required' { + is_required = true + } + else {} } } + if is_skip { + continue + } field_type := g.typ(field.typ) field_sym := g.table.get_type_symbol(field.typ) // First generate decoding - if field.attrs.contains('raw') { + if is_raw { dec.writeln('\tres.${c_name(field.name)} = tos5(cJSON_PrintUnformatted(' + 'js_get(root, "$name")));') } else { @@ -135,35 +149,36 @@ fn (mut g Gen) gen_struct_enc_dec(type_info ast.TypeInfo, styp string, mut enc s g.gen_json_for_type(field.typ) dec_name := js_dec_name(field_type) if is_js_prim(field_type) { - dec.writeln('\tres.${c_name(field.name)} = $dec_name (js_get(root, "$name"));') + tmp := g.new_tmp_var() + gen_js_get(styp, tmp, name, mut dec, is_required) + dec.writeln('\tres.${c_name(field.name)} = $dec_name (jsonroot_$tmp);') } else if field_sym.kind == .enum_ { - dec.writeln('\tres.${c_name(field.name)} = json__decode_u64(js_get(root, "$name"));') + tmp := g.new_tmp_var() + gen_js_get(styp, tmp, name, mut dec, is_required) + dec.writeln('\tres.${c_name(field.name)} = json__decode_u64(jsonroot_$tmp);') } else if field_sym.name == 'time.Time' { // time struct requires special treatment // it has to be decoded from a unix timestamp number - dec.writeln('\tres.${c_name(field.name)} = time__unix(json__decode_u64(js_get(root, "$name")));') + tmp := g.new_tmp_var() + gen_js_get(styp, tmp, name, mut dec, is_required) + dec.writeln('\tres.${c_name(field.name)} = time__unix(json__decode_u64(jsonroot_$tmp));') } else if field_sym.kind == .alias { alias := field_sym.info as ast.Alias parent_type := g.typ(alias.parent_type) parent_dec_name := js_dec_name(parent_type) if is_js_prim(parent_type) { - dec.writeln('\tres.${c_name(field.name)} = $parent_dec_name (js_get(root, "$name"));') + tmp := g.new_tmp_var() + gen_js_get(styp, tmp, name, mut dec, is_required) + dec.writeln('\tres.${c_name(field.name)} = $parent_dec_name (jsonroot_$tmp);') } else { g.gen_json_for_type(field.typ) tmp := g.new_tmp_var() - dec.writeln('\tOption_$field_type $tmp = $dec_name (js_get(root,"$name"));') - dec.writeln('\tif(${tmp}.state != 0) {') - dec.writeln('\t\treturn (Option_$styp){ .state = ${tmp}.state, .err = ${tmp}.err, .data = {0} };') - dec.writeln('\t}') + gen_js_get_opt(dec_name, field_type, styp, tmp, name, mut dec, is_required) dec.writeln('\tres.${c_name(field.name)} = *($field_type*) ${tmp}.data;') } } else { - // dec.writeln(' $dec_name (js_get(root, "$name"), & (res . $field.name));') tmp := g.new_tmp_var() - dec.writeln('\tOption_$field_type $tmp = $dec_name (js_get(root,"$name"));') - dec.writeln('\tif(${tmp}.state != 0) {') - dec.writeln('\t\treturn (Option_$styp){ .state = ${tmp}.state, .err = ${tmp}.err, .data = {0} };') - dec.writeln('\t}') + gen_js_get_opt(dec_name, field_type, styp, tmp, name, mut dec, is_required) dec.writeln('\tres.${c_name(field.name)} = *($field_type*) ${tmp}.data;') } } @@ -189,6 +204,23 @@ fn (mut g Gen) gen_struct_enc_dec(type_info ast.TypeInfo, styp string, mut enc s } } +fn gen_js_get(styp string, tmp string, name string, mut dec strings.Builder, is_required bool) { + dec.writeln('\tcJSON *jsonroot_$tmp = js_get(root,"$name");') + if is_required { + dec.writeln('\tif(jsonroot_$tmp == 0) {') + dec.writeln('\t\treturn (Option_$styp){ .state = 2, .err = _v_error(_SLIT("expected field \'$name\' is missing")), .data = {0} };') + dec.writeln('\t}') + } +} + +fn gen_js_get_opt(dec_name string, field_type string, styp string, tmp string, name string, mut dec strings.Builder, is_required bool) { + gen_js_get(styp, tmp, name, mut dec, is_required) + dec.writeln('\tOption_$field_type $tmp = $dec_name (jsonroot_$tmp);') + dec.writeln('\tif(${tmp}.state != 0) {') + dec.writeln('\t\treturn (Option_$styp){ .state = ${tmp}.state, .err = ${tmp}.err, .data = {0} };') + dec.writeln('\t}') +} + fn js_enc_name(typ string) string { suffix := if typ.ends_with('*') { typ.replace('*', '') } else { typ } name := 'json__encode_$suffix'