Commit 7dacf77745

Jonathan Marler <johnnymarler@gmail.com>
2023-08-06 05:56:00
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.
1 parent 68f8496
Changed files (2)
lib/std/json/stringify.zig
@@ -33,6 +33,9 @@ pub const StringifyOptions = struct {
 
     /// Should unicode characters be escaped in strings?
     escape_unicode: bool = false,
+
+    /// When true, renders numbers outside the range `±1<<53` (the precise integer range of f64) as JSON strings in base 10.
+    emit_big_numbers_quoted: bool = false,
 };
 
 /// Writes the given value to the `std.io.Writer` stream.
@@ -161,7 +164,7 @@ pub fn writeStreamArbitraryDepth(
 ///  * Zig `bool` -> JSON `true` or `false`.
 ///  * Zig `?T` -> `null` or the rendering of `T`.
 ///  * Zig `i32`, `u64`, etc. -> JSON number or string.
-///      * 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.
+///      * 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.
 ///  * Zig floats -> JSON number or string.
 ///      * If the value cannot be precisely represented by an f64, it is rendered as a JSON string. Otherwise, it is rendered as JSON number.
 ///      * TODO: Float rendering will likely change in the future, e.g. to remove the unnecessary "e+00".
@@ -400,20 +403,16 @@ pub fn WriteStream(
             const T = @TypeOf(value);
             switch (@typeInfo(T)) {
                 .Int => |info| {
-                    if (info.bits < 53) {
-                        try self.valueStart();
-                        try self.stream.print("{}", .{value});
-                        self.valueDone();
-                        return;
-                    }
-                    if (value < 4503599627370496 and (info.signedness == .unsigned or value > -4503599627370496)) {
-                        try self.valueStart();
+                    const emit_unquoted =
+                        if (!self.options.emit_big_numbers_quoted) true
+                        else if (info.bits < 53) true
+                        else (value < 4503599627370496 and (info.signedness == .unsigned or value > -4503599627370496));
+                    try self.valueStart();
+                    if (emit_unquoted) {
                         try self.stream.print("{}", .{value});
-                        self.valueDone();
-                        return;
+                    } else {
+                        try self.stream.print("\"{}\"", .{value});
                     }
-                    try self.valueStart();
-                    try self.stream.print("\"{}\"", .{value});
                     self.valueDone();
                     return;
                 },
lib/std/json/stringify_test.zig
@@ -126,6 +126,7 @@ test "stringify basic types" {
     try testStringify("4.2e+01", 42.0, .{});
     try testStringify("42", @as(u8, 42), .{});
     try testStringify("42", @as(u128, 42), .{});
+    try testStringify("9999999999999999", 9999999999999999, .{});
     try testStringify("4.2e+01", @as(f32, 42), .{});
     try testStringify("4.2e+01", @as(f64, 42), .{});
     try testStringify("\"ItBroke\"", @as(anyerror, error.ItBroke), .{});
@@ -432,3 +433,8 @@ test "print" {
     ;
     try std.testing.expectEqualStrings(expected, result);
 }
+
+test "big integers" {
+    try testStringify("9999999999999999", 9999999999999999, .{});
+    try testStringify("\"9999999999999999\"", 9999999999999999, .{ .emit_big_numbers_quoted = true });
+}