authorgravatar for johnnymarler@gmail.comJonathan Marler <johnnymarler@gmail.com> 2023-08-05 21:56:00-06:00
committergravatar for johnnymarler@gmail.comJonathan Marler <johnnymarler@gmail.com> 2023-08-05 21:56:00-06:00
log7dacf7774523a454fce7216c13eb03c6badf8d7b
tree4ae025fb9f546d09d3edeaada625dc89f880eca3
parent68f84964b3d80e5b976810208a14c31268a181e1

std.json: fix roundtrip stringify for large integers

std.json follows interoperability recommendations from RFC8259 to limit JSON number values to those that fit inside an f64. However, since Zig supports arbitrarily large JSON numbers, this breaks roundtrip data congruence. To appease both use cases, I've added an option `emit_big_numbers_quoted` to StringifyOptions. It's disabled by default which preserves roundtrip but can be enabled to favor interoperability.

2 files changed, 18 insertions(+), 13 deletions(-)

lib/std/json/stringify.zig+12-13
...@@ -33,6 +33,9 @@ pub const StringifyOptions = struct {...@@ -33,6 +33,9 @@ pub const StringifyOptions = struct {
3333
34 /// Should unicode characters be escaped in strings?34 /// Should unicode characters be escaped in strings?
35 escape_unicode: bool = false,35 escape_unicode: bool = false,
36
37 /// When true, renders numbers outside the range `±1<<53` (the precise integer range of f64) as JSON strings in base 10.
38 emit_big_numbers_quoted: bool = false,
36};39};
3740
38/// Writes the given value to the `std.io.Writer` stream.41/// Writes the given value to the `std.io.Writer` stream.
...@@ -161,7 +164,7 @@ pub fn writeStreamArbitraryDepth(...@@ -161,7 +164,7 @@ pub fn writeStreamArbitraryDepth(
161/// * Zig `bool` -> JSON `true` or `false`.164/// * Zig `bool` -> JSON `true` or `false`.
162/// * Zig `?T` -> `null` or the rendering of `T`.165/// * Zig `?T` -> `null` or the rendering of `T`.
163/// * Zig `i32`, `u64`, etc. -> JSON number or string.166/// * Zig `i32`, `u64`, etc. -> JSON number or string.
164/// * If the value is outside the range `±1<<53` (the precise integer rage of f64), it is rendered as a JSON string in base 10. Otherwise, it is rendered as JSON number.167/// * If the value is outside the range `±1<<53` (the precise integer range of f64), it is rendered as a JSON string in base 10. Otherwise, it is rendered as JSON number.
165/// * Zig floats -> JSON number or string.168/// * Zig floats -> JSON number or string.
166/// * If the value cannot be precisely represented by an f64, it is rendered as a JSON string. Otherwise, it is rendered as JSON number.169/// * If the value cannot be precisely represented by an f64, it is rendered as a JSON string. Otherwise, it is rendered as JSON number.
167/// * TODO: Float rendering will likely change in the future, e.g. to remove the unnecessary "e+00".170/// * TODO: Float rendering will likely change in the future, e.g. to remove the unnecessary "e+00".
...@@ -400,20 +403,16 @@ pub fn WriteStream(...@@ -400,20 +403,16 @@ pub fn WriteStream(
400 const T = @TypeOf(value);403 const T = @TypeOf(value);
401 switch (@typeInfo(T)) {404 switch (@typeInfo(T)) {
402 .Int => |info| {405 .Int => |info| {
403 if (info.bits < 53) {406 const emit_unquoted =
404 try self.valueStart();407 if (!self.options.emit_big_numbers_quoted) true
405 try self.stream.print("{}", .{value});408 else if (info.bits < 53) true
406 self.valueDone();409 else (value < 4503599627370496 and (info.signedness == .unsigned or value > -4503599627370496));
407 return;410 try self.valueStart();
408 }411 if (emit_unquoted) {
409 if (value < 4503599627370496 and (info.signedness == .unsigned or value > -4503599627370496)) {
410 try self.valueStart();
411 try self.stream.print("{}", .{value});412 try self.stream.print("{}", .{value});
412 self.valueDone();413 } else {
413 return;414 try self.stream.print("\"{}\"", .{value});
414 }415 }
415 try self.valueStart();
416 try self.stream.print("\"{}\"", .{value});
417 self.valueDone();416 self.valueDone();
418 return;417 return;
419 },418 },
lib/std/json/stringify_test.zig+6
...@@ -126,6 +126,7 @@ test "stringify basic types" {...@@ -126,6 +126,7 @@ test "stringify basic types" {
126 try testStringify("4.2e+01", 42.0, .{});126 try testStringify("4.2e+01", 42.0, .{});
127 try testStringify("42", @as(u8, 42), .{});127 try testStringify("42", @as(u8, 42), .{});
128 try testStringify("42", @as(u128, 42), .{});128 try testStringify("42", @as(u128, 42), .{});
129 try testStringify("9999999999999999", 9999999999999999, .{});
129 try testStringify("4.2e+01", @as(f32, 42), .{});130 try testStringify("4.2e+01", @as(f32, 42), .{});
130 try testStringify("4.2e+01", @as(f64, 42), .{});131 try testStringify("4.2e+01", @as(f64, 42), .{});
131 try testStringify("\"ItBroke\"", @as(anyerror, error.ItBroke), .{});132 try testStringify("\"ItBroke\"", @as(anyerror, error.ItBroke), .{});
...@@ -432,3 +433,8 @@ test "print" {...@@ -432,3 +433,8 @@ test "print" {
432 ;433 ;
433 try std.testing.expectEqualStrings(expected, result);434 try std.testing.expectEqualStrings(expected, result);
434}435}
436
437test "big integers" {
438 try testStringify("9999999999999999", 9999999999999999, .{});
439 try testStringify("\"9999999999999999\"", 9999999999999999, .{ .emit_big_numbers_quoted = true });
440}