From 6f6c7ed153432cc8813a13fbd9869aa9d889f7ec Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 25 Mar 2026 16:42:18 +0300 Subject: [PATCH] json: fix inconsistent serialization/deserialization for u64 (fixes #25605) --- vlib/json/README.md | 5 +- vlib/json/json_primitives.c.v | 274 ++++++++++++++++++++++++++++- vlib/json/tests/json_decode_test.v | 38 ++++ 3 files changed, 310 insertions(+), 7 deletions(-) diff --git a/vlib/json/README.md b/vlib/json/README.md index 478b41266..31e1a4961 100644 --- a/vlib/json/README.md +++ b/vlib/json/README.md @@ -4,6 +4,9 @@ The `json` module provides encoding/decoding of V data structures to/from JSON. For more details, see also the [JSON section of the V documentation](https://github.com/vlang/v/blob/master/doc/docs.md#json) +Large `i64` and `u64` values are encoded as exact decimal JSON numbers, and +`json.decode` preserves those integer values when reading decimal integer input. + ## Examples Here is an example of encoding and decoding a V struct with several fields. @@ -46,4 +49,4 @@ fn main() { println('JSON encoding of employee y: ${ss}') assert ss == s } -``` \ No newline at end of file +``` diff --git a/vlib/json/json_primitives.c.v b/vlib/json/json_primitives.c.v index 29d54c720..5f7eb57b4 100644 --- a/vlib/json/json_primitives.c.v +++ b/vlib/json/json_primitives.c.v @@ -4,6 +4,7 @@ module json import math +import strconv #flag -I @VEXEROOT/thirdparty/cJSON #flag @VEXEROOT/thirdparty/cJSON/cJSON.o @@ -20,12 +21,20 @@ $if windows { } pub struct C.cJSON { + next &C.cJSON + prev &C.cJSON + child &C.cJSON + type int + valuestring &char valueint int valuedouble f64 - valuestring &char } fn C.cJSON_IsTrue(&C.cJSON) bool +fn C.cJSON_IsFalse(&C.cJSON) bool +fn C.cJSON_IsNull(&C.cJSON) bool +fn C.cJSON_IsNumber(&C.cJSON) bool +fn C.cJSON_IsString(&C.cJSON) bool fn C.cJSON_IsObject(&C.cJSON) bool fn C.cJSON_IsArray(&C.cJSON) bool @@ -45,6 +54,8 @@ fn C.cJSON_PrintUnformatted(&C.cJSON) &char fn C.cJSON_Print(&C.cJSON) &char fn C.cJSON_free(voidptr) +fn C.malloc(usize) voidptr +fn C.memcpy(voidptr, voidptr, usize) voidptr // decode tries to decode the provided JSON string, into a V structure. // If it can not do that, it returns an error describing the reason for @@ -95,6 +106,9 @@ fn decode_i64(root &C.cJSON) i64 { if isnil(root) { return i64(0) } + if value := decode_exact_i64(root) { + return value + } return i64(root.valuedouble) // i64 is double in C } @@ -133,6 +147,9 @@ fn decode_u64(root &C.cJSON) u64 { if isnil(root) { return u64(0) } + if value := decode_exact_u64(root) { + return value + } return u64(root.valuedouble) } @@ -157,7 +174,7 @@ fn decode_rune(root &C.cJSON) rune { if isnil(root) { return rune(0) } - if isnil(root.valuestring) { + if isnil(root.valuestring) || !C.cJSON_IsString(root) { return rune(0) } @@ -170,7 +187,7 @@ fn decode_string(root &C.cJSON) string { if isnil(root) { return '' } - if !isnil(root.valuestring) { + if !isnil(root.valuestring) && C.cJSON_IsString(root) { return unsafe { tos_clone(&u8(root.valuestring)) } // , _strlen(root.valuestring)) } // Object/array values can be stringified JSON payloads (e.g. `{}` from JSON.stringify()). @@ -207,7 +224,8 @@ fn encode_i16(val i16) &C.cJSON { @[markused] fn encode_i64(val i64) &C.cJSON { - return C.cJSON_CreateNumber(val) + lit := val.str() + return C.cJSON_CreateRaw(&char(lit.str)) } // TODO: remove when `byte` is removed @@ -233,7 +251,8 @@ fn encode_u32(val u32) &C.cJSON { @[markused] fn encode_u64(val u64) &C.cJSON { - return C.cJSON_CreateNumber(val) + lit := val.str() + return C.cJSON_CreateRaw(&char(lit.str)) } @[markused] @@ -333,11 +352,254 @@ fn json_float_to_raw_string[T](val T) string { return raw } +fn decode_exact_i64(root &C.cJSON) ?i64 { + if isnil(root) || isnil(root.valuestring) { + return none + } + lit := unsafe { tos_clone(&u8(root.valuestring)) } + value := strconv.parse_int(lit, 10, 64) or { return none } + return value +} + +fn decode_exact_u64(root &C.cJSON) ?u64 { + if isnil(root) || isnil(root.valuestring) { + return none + } + lit := unsafe { tos_clone(&u8(root.valuestring)) } + value := strconv.parse_uint(lit, 10, 64) or { return none } + return value +} + +fn clone_cstring(s string) &char { + buf := C.malloc(usize(s.len + 1)) + if buf == unsafe { nil } { + return unsafe { nil } + } + unsafe { + if s.len > 0 { + C.memcpy(buf, s.str, usize(s.len)) + } + (&u8(buf))[s.len] = 0 + } + return &char(buf) +} + +@[inline] +fn is_json_whitespace(ch u8) bool { + return ch == ` ` || ch == `\n` || ch == `\r` || ch == `\t` +} + +fn skip_json_whitespace(src string, idx int) int { + mut pos := idx + for pos < src.len && is_json_whitespace(src[pos]) { + pos++ + } + return pos +} + +fn skip_json_string(src string, idx int) int { + mut pos := idx + if pos >= src.len || src[pos] != `"` { + return -1 + } + pos++ + for pos < src.len { + match src[pos] { + `"` { + return pos + 1 + } + `\\` { + pos++ + if pos >= src.len { + return -1 + } + if src[pos] == `u` { + pos += 5 + } else { + pos++ + } + } + else { + pos++ + } + } + } + return -1 +} + +fn skip_json_literal(src string, lit string, idx int) int { + if idx + lit.len > src.len || src[idx..idx + lit.len] != lit { + return -1 + } + return idx + lit.len +} + +fn skip_json_number(src string, idx int) (string, int) { + mut pos := idx + start := pos + if pos < src.len && src[pos] == `-` { + pos++ + } + if pos >= src.len { + return '', -1 + } + if src[pos] == `0` { + pos++ + } else { + if !src[pos].is_digit() { + return '', -1 + } + for pos < src.len && src[pos].is_digit() { + pos++ + } + } + if pos < src.len && src[pos] == `.` { + pos++ + if pos >= src.len || !src[pos].is_digit() { + return '', -1 + } + for pos < src.len && src[pos].is_digit() { + pos++ + } + } + if pos < src.len && (src[pos] == `e` || src[pos] == `E`) { + pos++ + if pos < src.len && (src[pos] == `+` || src[pos] == `-`) { + pos++ + } + if pos >= src.len || !src[pos].is_digit() { + return '', -1 + } + for pos < src.len && src[pos].is_digit() { + pos++ + } + } + return src[start..pos], pos +} + +fn annotate_json_number_literal(root &C.cJSON, lit string) { + if lit.len == 0 || !isnil(root.valuestring) { + return + } + unsafe { + root.valuestring = clone_cstring(lit) + } +} + +fn annotate_json_value(root &C.cJSON, src string, idx int) int { + if isnil(root) { + return -1 + } + mut pos := skip_json_whitespace(src, idx) + if C.cJSON_IsObject(root) { + if pos >= src.len || src[pos] != `{` { + return -1 + } + pos++ + pos = skip_json_whitespace(src, pos) + mut child := root.child + if isnil(child) { + if pos >= src.len || src[pos] != `}` { + return -1 + } + return pos + 1 + } + for !isnil(child) { + pos = skip_json_string(src, pos) + if pos == -1 { + return -1 + } + pos = skip_json_whitespace(src, pos) + if pos >= src.len || src[pos] != `:` { + return -1 + } + pos++ + pos = annotate_json_value(child, src, pos) + if pos == -1 { + return -1 + } + pos = skip_json_whitespace(src, pos) + if !isnil(child.next) { + if pos >= src.len || src[pos] != `,` { + return -1 + } + pos++ + } + child = child.next + } + if pos >= src.len || src[pos] != `}` { + return -1 + } + return pos + 1 + } + if C.cJSON_IsArray(root) { + if pos >= src.len || src[pos] != `[` { + return -1 + } + pos++ + pos = skip_json_whitespace(src, pos) + mut child := root.child + if isnil(child) { + if pos >= src.len || src[pos] != `]` { + return -1 + } + return pos + 1 + } + for !isnil(child) { + pos = annotate_json_value(child, src, pos) + if pos == -1 { + return -1 + } + pos = skip_json_whitespace(src, pos) + if !isnil(child.next) { + if pos >= src.len || src[pos] != `,` { + return -1 + } + pos++ + } + child = child.next + } + if pos >= src.len || src[pos] != `]` { + return -1 + } + return pos + 1 + } + if C.cJSON_IsString(root) { + return skip_json_string(src, pos) + } + if C.cJSON_IsNumber(root) { + lit, next_pos := skip_json_number(src, pos) + if next_pos == -1 { + return -1 + } + annotate_json_number_literal(root, lit) + return next_pos + } + if C.cJSON_IsTrue(root) { + return skip_json_literal(src, 'true', pos) + } + if C.cJSON_IsFalse(root) { + return skip_json_literal(src, 'false', pos) + } + if C.cJSON_IsNull(root) { + return skip_json_literal(src, 'null', pos) + } + return -1 +} + +fn annotate_json_number_literals(root &C.cJSON, src string) { + _ := annotate_json_value(root, src, 0) +} + // /////////////////////// // user := decode_User(json_parse(js_string_var)) @[markused] fn json_parse(s string) &C.cJSON { - return C.cJSON_Parse(&char(s.str)) + root := C.cJSON_Parse(&char(s.str)) + if !isnil(root) { + annotate_json_number_literals(root, s) + } + return root } // json_string := json_print(encode_User(user)) diff --git a/vlib/json/tests/json_decode_test.v b/vlib/json/tests/json_decode_test.v index 06662e3ba..4fef0b2cd 100644 --- a/vlib/json/tests/json_decode_test.v +++ b/vlib/json/tests/json_decode_test.v @@ -45,6 +45,44 @@ fn test_decode_u64() { // println(m) } +fn test_decode_large_u64_from_decimal_json() { + cases := [ + u64(9007199254740991), + u64(9007199254740992), + u64(9007199254740993), + u64(9223372036854775807), + u64(9223372036854775808), + u64(9223372036854775809), + u64(18446744073709551614), + u64(18446744073709551615), + ] + for want in cases { + got := json.decode([]u64, '[${want}]')! + assert got.len == 1 + assert got[0] == want + } +} + +fn test_encode_decode_large_u64_roundtrip() { + cases := [ + u64(9007199254740991), + u64(9007199254740992), + u64(9007199254740993), + u64(9223372036854775807), + u64(9223372036854775808), + u64(9223372036854775809), + u64(18446744073709551614), + u64(18446744073709551615), + ] + for want in cases { + encoded := json.encode([want]) + assert encoded == '[${want}]' + got := json.decode([]u64, encoded)! + assert got.len == 1 + assert got[0] == want + } +} + // pub struct Comment { -- 2.39.5