master
1//! Writes JSON ([RFC8259](https://tools.ietf.org/html/rfc8259)) formatted data
2//! to a stream.
3//!
4//! The sequence of method calls to write JSON content must follow this grammar:
5//! ```
6//! <once> = <value>
7//! <value> =
8//! | <object>
9//! | <array>
10//! | write
11//! | print
12//! | <writeRawStream>
13//! <object> = beginObject ( <field> <value> )* endObject
14//! <field> = objectField | objectFieldRaw | <objectFieldRawStream>
15//! <array> = beginArray ( <value> )* endArray
16//! <writeRawStream> = beginWriteRaw ( stream.writeAll )* endWriteRaw
17//! <objectFieldRawStream> = beginObjectFieldRaw ( stream.writeAll )* endObjectFieldRaw
18//! ```
19
20const std = @import("../std.zig");
21const assert = std.debug.assert;
22const Allocator = std.mem.Allocator;
23const ArrayList = std.ArrayList;
24const BitStack = std.BitStack;
25const Stringify = @This();
26const Writer = std.Io.Writer;
27
28const IndentationMode = enum(u1) {
29 object = 0,
30 array = 1,
31};
32
33writer: *Writer,
34options: Options = .{},
35indent_level: usize = 0,
36next_punctuation: enum {
37 the_beginning,
38 none,
39 comma,
40 colon,
41} = .the_beginning,
42
43nesting_stack: switch (safety_checks) {
44 .checked_to_fixed_depth => |fixed_buffer_size| [(fixed_buffer_size + 7) >> 3]u8,
45 .assumed_correct => void,
46} = switch (safety_checks) {
47 .checked_to_fixed_depth => @splat(0),
48 .assumed_correct => {},
49},
50
51raw_streaming_mode: if (build_mode_has_safety)
52 enum { none, value, objectField }
53else
54 void = if (build_mode_has_safety) .none else {},
55
56const build_mode_has_safety = switch (@import("builtin").mode) {
57 .Debug, .ReleaseSafe => true,
58 .ReleaseFast, .ReleaseSmall => false,
59};
60
61/// The `safety_checks_hint` parameter determines how much memory is used to enable assertions that the above grammar is being followed,
62/// e.g. tripping an assertion rather than allowing `endObject` to emit the final `}` in `[[[]]}`.
63/// "Depth" in this context means the depth of nested `[]` or `{}` expressions
64/// (or equivalently the amount of recursion on the `<value>` grammar expression above).
65/// For example, emitting the JSON `[[[]]]` requires a depth of 3.
66/// If `.checked_to_fixed_depth` is used, there is additionally an assertion that the nesting depth never exceeds the given limit.
67/// `.checked_to_fixed_depth` embeds the storage required in the `Stringify` struct.
68/// `.assumed_correct` requires no space and performs none of these assertions.
69/// In `ReleaseFast` and `ReleaseSmall` mode, the given `safety_checks_hint` is ignored and is always treated as `.assumed_correct`.
70const safety_checks_hint: union(enum) {
71 /// Rounded up to the nearest multiple of 8.
72 checked_to_fixed_depth: usize,
73 assumed_correct,
74} = .{ .checked_to_fixed_depth = 256 };
75
76const safety_checks: @TypeOf(safety_checks_hint) = if (build_mode_has_safety)
77 safety_checks_hint
78else
79 .assumed_correct;
80
81pub const Error = Writer.Error;
82
83pub fn beginArray(self: *Stringify) Error!void {
84 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
85 try self.valueStart();
86 try self.writer.writeByte('[');
87 try self.pushIndentation(.array);
88 self.next_punctuation = .none;
89}
90
91pub fn beginObject(self: *Stringify) Error!void {
92 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
93 try self.valueStart();
94 try self.writer.writeByte('{');
95 try self.pushIndentation(.object);
96 self.next_punctuation = .none;
97}
98
99pub fn endArray(self: *Stringify) Error!void {
100 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
101 self.popIndentation(.array);
102 switch (self.next_punctuation) {
103 .none => {},
104 .comma => {
105 try self.indent();
106 },
107 .the_beginning, .colon => unreachable,
108 }
109 try self.writer.writeByte(']');
110 self.valueDone();
111}
112
113pub fn endObject(self: *Stringify) Error!void {
114 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
115 self.popIndentation(.object);
116 switch (self.next_punctuation) {
117 .none => {},
118 .comma => {
119 try self.indent();
120 },
121 .the_beginning, .colon => unreachable,
122 }
123 try self.writer.writeByte('}');
124 self.valueDone();
125}
126
127fn pushIndentation(self: *Stringify, mode: IndentationMode) !void {
128 switch (safety_checks) {
129 .checked_to_fixed_depth => {
130 BitStack.pushWithStateAssumeCapacity(&self.nesting_stack, &self.indent_level, @intFromEnum(mode));
131 },
132 .assumed_correct => {
133 self.indent_level += 1;
134 },
135 }
136}
137fn popIndentation(self: *Stringify, expected_mode: IndentationMode) void {
138 switch (safety_checks) {
139 .checked_to_fixed_depth => {
140 assert(BitStack.popWithState(&self.nesting_stack, &self.indent_level) == @intFromEnum(expected_mode));
141 },
142 .assumed_correct => {
143 self.indent_level -= 1;
144 },
145 }
146}
147
148fn indent(self: *Stringify) !void {
149 var char: u8 = ' ';
150 const n_chars = switch (self.options.whitespace) {
151 .minified => return,
152 .indent_1 => 1 * self.indent_level,
153 .indent_2 => 2 * self.indent_level,
154 .indent_3 => 3 * self.indent_level,
155 .indent_4 => 4 * self.indent_level,
156 .indent_8 => 8 * self.indent_level,
157 .indent_tab => blk: {
158 char = '\t';
159 break :blk self.indent_level;
160 },
161 };
162 try self.writer.writeByte('\n');
163 try self.writer.splatByteAll(char, n_chars);
164}
165
166fn valueStart(self: *Stringify) !void {
167 if (self.isObjectKeyExpected()) |is_it| assert(!is_it); // Call objectField*(), not write(), for object keys.
168 return self.valueStartAssumeTypeOk();
169}
170fn objectFieldStart(self: *Stringify) !void {
171 if (self.isObjectKeyExpected()) |is_it| assert(is_it); // Expected write(), not objectField*().
172 return self.valueStartAssumeTypeOk();
173}
174fn valueStartAssumeTypeOk(self: *Stringify) !void {
175 assert(!self.isComplete()); // JSON document already complete.
176 switch (self.next_punctuation) {
177 .the_beginning => {
178 // No indentation for the very beginning.
179 },
180 .none => {
181 // First item in a container.
182 try self.indent();
183 },
184 .comma => {
185 // Subsequent item in a container.
186 try self.writer.writeByte(',');
187 try self.indent();
188 },
189 .colon => {
190 try self.writer.writeByte(':');
191 if (self.options.whitespace != .minified) {
192 try self.writer.writeByte(' ');
193 }
194 },
195 }
196}
197fn valueDone(self: *Stringify) void {
198 self.next_punctuation = .comma;
199}
200
201// Only when safety is enabled:
202fn isObjectKeyExpected(self: *const Stringify) ?bool {
203 switch (safety_checks) {
204 .checked_to_fixed_depth => return self.indent_level > 0 and
205 BitStack.peekWithState(&self.nesting_stack, self.indent_level) == @intFromEnum(IndentationMode.object) and
206 self.next_punctuation != .colon,
207 .assumed_correct => return null,
208 }
209}
210fn isComplete(self: *const Stringify) bool {
211 return self.indent_level == 0 and self.next_punctuation == .comma;
212}
213
214/// An alternative to calling `write` that formats a value with `std.fmt`.
215/// This function does the usual punctuation and indentation formatting
216/// assuming the resulting formatted string represents a single complete value;
217/// e.g. `"1"`, `"[]"`, `"[1,2]"`, not `"1,2"`.
218/// This function may be useful for doing your own number formatting.
219pub fn print(self: *Stringify, comptime fmt: []const u8, args: anytype) Error!void {
220 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
221 try self.valueStart();
222 try self.writer.print(fmt, args);
223 self.valueDone();
224}
225
226test print {
227 var out_buf: [1024]u8 = undefined;
228 var out: Writer = .fixed(&out_buf);
229
230 var w: Stringify = .{ .writer = &out, .options = .{ .whitespace = .indent_2 } };
231
232 try w.beginObject();
233 try w.objectField("a");
234 try w.print("[ ]", .{});
235 try w.objectField("b");
236 try w.beginArray();
237 try w.print("[{s}] ", .{"[]"});
238 try w.print(" {}", .{12345});
239 try w.endArray();
240 try w.endObject();
241
242 const expected =
243 \\{
244 \\ "a": [ ],
245 \\ "b": [
246 \\ [[]] ,
247 \\ 12345
248 \\ ]
249 \\}
250 ;
251 try std.testing.expectEqualStrings(expected, out.buffered());
252}
253
254/// An alternative to calling `write` that allows you to write directly to the `.writer` field, e.g. with `.writer.writeAll()`.
255/// Call `beginWriteRaw()`, then write a complete value (including any quotes if necessary) directly to the `.writer` field,
256/// then call `endWriteRaw()`.
257/// This can be useful for streaming very long strings into the output without needing it all buffered in memory.
258pub fn beginWriteRaw(self: *Stringify) !void {
259 if (build_mode_has_safety) {
260 assert(self.raw_streaming_mode == .none);
261 self.raw_streaming_mode = .value;
262 }
263 try self.valueStart();
264}
265
266/// See `beginWriteRaw`.
267pub fn endWriteRaw(self: *Stringify) void {
268 if (build_mode_has_safety) {
269 assert(self.raw_streaming_mode == .value);
270 self.raw_streaming_mode = .none;
271 }
272 self.valueDone();
273}
274
275/// See `Stringify` for when to call this method.
276/// `key` is the string content of the property name.
277/// Surrounding quotes will be added and any special characters will be escaped.
278/// See also `objectFieldRaw`.
279pub fn objectField(self: *Stringify, key: []const u8) Error!void {
280 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
281 try self.objectFieldStart();
282 try encodeJsonString(key, self.options, self.writer);
283 self.next_punctuation = .colon;
284}
285/// See `Stringify` for when to call this method.
286/// `quoted_key` is the complete bytes of the key including quotes and any necessary escape sequences.
287/// A few assertions are performed on the given value to ensure that the caller of this function understands the API contract.
288/// See also `objectField`.
289pub fn objectFieldRaw(self: *Stringify, quoted_key: []const u8) Error!void {
290 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
291 assert(quoted_key.len >= 2 and quoted_key[0] == '"' and quoted_key[quoted_key.len - 1] == '"'); // quoted_key should be "quoted".
292 try self.objectFieldStart();
293 try self.writer.writeAll(quoted_key);
294 self.next_punctuation = .colon;
295}
296
297/// In the rare case that you need to write very long object field names,
298/// this is an alternative to `objectField` and `objectFieldRaw` that allows you to write directly to the `.writer` field
299/// similar to `beginWriteRaw`.
300/// Call `endObjectFieldRaw()` when you're done.
301pub fn beginObjectFieldRaw(self: *Stringify) !void {
302 if (build_mode_has_safety) {
303 assert(self.raw_streaming_mode == .none);
304 self.raw_streaming_mode = .objectField;
305 }
306 try self.objectFieldStart();
307}
308
309/// See `beginObjectFieldRaw`.
310pub fn endObjectFieldRaw(self: *Stringify) void {
311 if (build_mode_has_safety) {
312 assert(self.raw_streaming_mode == .objectField);
313 self.raw_streaming_mode = .none;
314 }
315 self.next_punctuation = .colon;
316}
317
318/// Renders the given Zig value as JSON.
319///
320/// Supported types:
321/// * Zig `bool` -> JSON `true` or `false`.
322/// * Zig `?T` -> `null` or the rendering of `T`.
323/// * Zig `i32`, `u64`, etc. -> JSON number or string.
324/// * When option `emit_nonportable_numbers_as_strings` is true, 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.
325/// * Zig floats -> JSON number or string.
326/// * If the value cannot be precisely represented by an f64, it is rendered as a JSON string. Otherwise, it is rendered as JSON number.
327/// * TODO: Float rendering will likely change in the future, e.g. to remove the unnecessary "e+00".
328/// * Zig `[]const u8`, `[]u8`, `*[N]u8`, `@Vector(N, u8)`, and similar -> JSON string.
329/// * See `Options.emit_strings_as_arrays`.
330/// * If the content is not valid UTF-8, rendered as an array of numbers instead.
331/// * Zig `[]T`, `[N]T`, `*[N]T`, `@Vector(N, T)`, and similar -> JSON array of the rendering of each item.
332/// * Zig tuple -> JSON array of the rendering of each item.
333/// * Zig `struct` -> JSON object with each field in declaration order.
334/// * If the struct declares a method `pub fn jsonStringify(self: *@This(), jw: anytype) !void`, it is called to do the serialization instead of the default behavior. The given `jw` is a pointer to this `Stringify`. See `std.json.Value` for an example.
335/// * See `Options.emit_null_optional_fields`.
336/// * Zig `union(enum)` -> JSON object with one field named for the active tag and a value representing the payload.
337/// * If the payload is `void`, then the emitted value is `{}`.
338/// * If the union declares a method `pub fn jsonStringify(self: *@This(), jw: anytype) !void`, it is called to do the serialization instead of the default behavior. The given `jw` is a pointer to this `Stringify`.
339/// * Zig `enum` -> JSON string naming the active tag.
340/// * If the enum declares a method `pub fn jsonStringify(self: *@This(), jw: anytype) !void`, it is called to do the serialization instead of the default behavior. The given `jw` is a pointer to this `Stringify`.
341/// * If the enum is non-exhaustive, unnamed values are rendered as integers.
342/// * Zig untyped enum literal -> JSON string naming the active tag.
343/// * Zig error -> JSON string naming the error.
344/// * Zig `*T` -> the rendering of `T`. Note there is no guard against circular-reference infinite recursion.
345///
346/// See also alternative functions `print` and `beginWriteRaw`.
347/// For writing object field names, use `objectField` instead.
348pub fn write(self: *Stringify, v: anytype) Error!void {
349 if (build_mode_has_safety) assert(self.raw_streaming_mode == .none);
350 const T = @TypeOf(v);
351 switch (@typeInfo(T)) {
352 .int => {
353 try self.valueStart();
354 if (self.options.emit_nonportable_numbers_as_strings and
355 (v <= -(1 << 53) or v >= (1 << 53)))
356 {
357 try self.writer.print("\"{}\"", .{v});
358 } else {
359 try self.writer.print("{}", .{v});
360 }
361 self.valueDone();
362 return;
363 },
364 .comptime_int => {
365 return self.write(@as(std.math.IntFittingRange(v, v), v));
366 },
367 .float, .comptime_float => {
368 if (@as(f64, @floatCast(v)) == v) {
369 try self.valueStart();
370 try self.writer.print("{}", .{@as(f64, @floatCast(v))});
371 self.valueDone();
372 return;
373 }
374 try self.valueStart();
375 try self.writer.print("\"{}\"", .{v});
376 self.valueDone();
377 return;
378 },
379
380 .bool => {
381 try self.valueStart();
382 try self.writer.writeAll(if (v) "true" else "false");
383 self.valueDone();
384 return;
385 },
386 .null => {
387 try self.valueStart();
388 try self.writer.writeAll("null");
389 self.valueDone();
390 return;
391 },
392 .optional => {
393 if (v) |payload| {
394 return try self.write(payload);
395 } else {
396 return try self.write(null);
397 }
398 },
399 .@"enum" => |enum_info| {
400 if (std.meta.hasFn(T, "jsonStringify")) {
401 return v.jsonStringify(self);
402 }
403
404 if (!enum_info.is_exhaustive) {
405 inline for (enum_info.fields) |field| {
406 if (v == @field(T, field.name)) {
407 break;
408 }
409 } else {
410 return self.write(@intFromEnum(v));
411 }
412 }
413
414 return self.stringValue(@tagName(v));
415 },
416 .enum_literal => {
417 return self.stringValue(@tagName(v));
418 },
419 .@"union" => {
420 if (std.meta.hasFn(T, "jsonStringify")) {
421 return v.jsonStringify(self);
422 }
423
424 const info = @typeInfo(T).@"union";
425 if (info.tag_type) |UnionTagType| {
426 try self.beginObject();
427 inline for (info.fields) |u_field| {
428 if (v == @field(UnionTagType, u_field.name)) {
429 try self.objectField(u_field.name);
430 if (u_field.type == void) {
431 // void v is {}
432 try self.beginObject();
433 try self.endObject();
434 } else {
435 try self.write(@field(v, u_field.name));
436 }
437 break;
438 }
439 } else {
440 unreachable; // No active tag?
441 }
442 try self.endObject();
443 return;
444 } else {
445 @compileError("Unable to stringify untagged union '" ++ @typeName(T) ++ "'");
446 }
447 },
448 .@"struct" => |S| {
449 if (std.meta.hasFn(T, "jsonStringify")) {
450 return v.jsonStringify(self);
451 }
452
453 if (S.is_tuple) {
454 try self.beginArray();
455 } else {
456 try self.beginObject();
457 }
458 inline for (S.fields) |Field| {
459 // don't include void fields
460 if (Field.type == void) continue;
461
462 var emit_field = true;
463
464 // don't include optional fields that are null when emit_null_optional_fields is set to false
465 if (@typeInfo(Field.type) == .optional) {
466 if (self.options.emit_null_optional_fields == false) {
467 if (@field(v, Field.name) == null) {
468 emit_field = false;
469 }
470 }
471 }
472
473 if (emit_field) {
474 if (!S.is_tuple) {
475 try self.objectField(Field.name);
476 }
477 try self.write(@field(v, Field.name));
478 }
479 }
480 if (S.is_tuple) {
481 try self.endArray();
482 } else {
483 try self.endObject();
484 }
485 return;
486 },
487 .error_set => return self.stringValue(@errorName(v)),
488 .pointer => |ptr_info| switch (ptr_info.size) {
489 .one => switch (@typeInfo(ptr_info.child)) {
490 .array => {
491 // Coerce `*[N]T` to `[]const T`.
492 const Slice = []const std.meta.Elem(ptr_info.child);
493 return self.write(@as(Slice, v));
494 },
495 else => {
496 return self.write(v.*);
497 },
498 },
499 .many, .slice => {
500 if (ptr_info.size == .many and ptr_info.sentinel() == null)
501 @compileError("unable to stringify type '" ++ @typeName(T) ++ "' without sentinel");
502 const slice = if (ptr_info.size == .many) std.mem.span(v) else v;
503
504 if (ptr_info.child == u8) {
505 // This is a []const u8, or some similar Zig string.
506 if (!self.options.emit_strings_as_arrays and std.unicode.utf8ValidateSlice(slice)) {
507 return self.stringValue(slice);
508 }
509 }
510
511 try self.beginArray();
512 for (slice) |x| {
513 try self.write(x);
514 }
515 try self.endArray();
516 return;
517 },
518 else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"),
519 },
520 .array => {
521 // Coerce `[N]T` to `*const [N]T` (and then to `[]const T`).
522 return self.write(&v);
523 },
524 .vector => |info| {
525 const array: [info.len]info.child = v;
526 return self.write(&array);
527 },
528 else => @compileError("Unable to stringify type '" ++ @typeName(T) ++ "'"),
529 }
530 unreachable;
531}
532
533fn stringValue(self: *Stringify, s: []const u8) !void {
534 try self.valueStart();
535 try encodeJsonString(s, self.options, self.writer);
536 self.valueDone();
537}
538
539pub const Options = struct {
540 /// Controls the whitespace emitted.
541 /// The default `.minified` is a compact encoding with no whitespace between tokens.
542 /// Any setting other than `.minified` will use newlines, indentation, and a space after each ':'.
543 /// `.indent_1` means 1 space for each indentation level, `.indent_2` means 2 spaces, etc.
544 /// `.indent_tab` uses a tab for each indentation level.
545 whitespace: enum {
546 minified,
547 indent_1,
548 indent_2,
549 indent_3,
550 indent_4,
551 indent_8,
552 indent_tab,
553 } = .minified,
554
555 /// Should optional fields with null value be written?
556 emit_null_optional_fields: bool = true,
557
558 /// Arrays/slices of u8 are typically encoded as JSON strings.
559 /// This option emits them as arrays of numbers instead.
560 /// Does not affect calls to `objectField*()`.
561 emit_strings_as_arrays: bool = false,
562
563 /// Should unicode characters be escaped in strings?
564 escape_unicode: bool = false,
565
566 /// When true, renders numbers outside the range `+-1<<53` (the precise integer range of f64) as JSON strings in base 10.
567 emit_nonportable_numbers_as_strings: bool = false,
568};
569
570/// Writes the given value to the `Writer` writer.
571/// See `Stringify` for how the given value is serialized into JSON.
572/// The maximum nesting depth of the output JSON document is 256.
573pub fn value(v: anytype, options: Options, writer: *Writer) Error!void {
574 var s: Stringify = .{ .writer = writer, .options = options };
575 try s.write(v);
576}
577
578test value {
579 var out: Writer.Allocating = .init(std.testing.allocator);
580 const writer = &out.writer;
581 defer out.deinit();
582
583 const T = struct { a: i32, b: []const u8 };
584 try value(T{ .a = 123, .b = "xy" }, .{}, writer);
585 try std.testing.expectEqualSlices(u8, "{\"a\":123,\"b\":\"xy\"}", out.written());
586
587 try testStringify("9999999999999999", 9999999999999999, .{});
588 try testStringify("\"9999999999999999\"", 9999999999999999, .{ .emit_nonportable_numbers_as_strings = true });
589
590 try testStringify("[1,1]", @as(@Vector(2, u32), @splat(1)), .{});
591 try testStringify("\"AA\"", @as(@Vector(2, u8), @splat('A')), .{});
592 try testStringify("[65,65]", @as(@Vector(2, u8), @splat('A')), .{ .emit_strings_as_arrays = true });
593
594 // void field
595 try testStringify("{\"foo\":42}", struct {
596 foo: u32,
597 bar: void = {},
598 }{ .foo = 42 }, .{});
599
600 const Tuple = struct { []const u8, usize };
601 try testStringify("[\"foo\",42]", Tuple{ "foo", 42 }, .{});
602
603 comptime {
604 testStringify("false", false, .{}) catch unreachable;
605 const MyStruct = struct { foo: u32 };
606 testStringify("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{
607 MyStruct{ .foo = 42 },
608 MyStruct{ .foo = 100 },
609 MyStruct{ .foo = 1000 },
610 }, .{}) catch unreachable;
611 }
612}
613
614/// Calls `value` and stores the result in dynamically allocated memory instead
615/// of taking a writer.
616///
617/// Caller owns returned memory.
618pub fn valueAlloc(gpa: Allocator, v: anytype, options: Options) error{OutOfMemory}![]u8 {
619 var aw: Writer.Allocating = .init(gpa);
620 defer aw.deinit();
621 value(v, options, &aw.writer) catch return error.OutOfMemory;
622 return aw.toOwnedSlice();
623}
624
625test valueAlloc {
626 const allocator = std.testing.allocator;
627 const expected =
628 \\{"foo":"bar","answer":42,"my_friend":"sammy"}
629 ;
630 const actual = try valueAlloc(allocator, .{ .foo = "bar", .answer = 42, .my_friend = "sammy" }, .{});
631 defer allocator.free(actual);
632
633 try std.testing.expectEqualStrings(expected, actual);
634}
635
636fn outputUnicodeEscape(codepoint: u21, w: *Writer) Error!void {
637 if (codepoint <= 0xFFFF) {
638 // If the character is in the Basic Multilingual Plane (U+0000 through U+FFFF),
639 // then it may be represented as a six-character sequence: a reverse solidus, followed
640 // by the lowercase letter u, followed by four hexadecimal digits that encode the character's code point.
641 try w.writeAll("\\u");
642 try w.printInt(codepoint, 16, .lower, .{ .width = 4, .fill = '0' });
643 } else {
644 assert(codepoint <= 0x10FFFF);
645 // To escape an extended character that is not in the Basic Multilingual Plane,
646 // the character is represented as a 12-character sequence, encoding the UTF-16 surrogate pair.
647 const high = @as(u16, @intCast((codepoint - 0x10000) >> 10)) + 0xD800;
648 const low = @as(u16, @intCast(codepoint & 0x3FF)) + 0xDC00;
649 try w.writeAll("\\u");
650 try w.printInt(high, 16, .lower, .{ .width = 4, .fill = '0' });
651 try w.writeAll("\\u");
652 try w.printInt(low, 16, .lower, .{ .width = 4, .fill = '0' });
653 }
654}
655
656fn outputSpecialEscape(c: u8, writer: *Writer) Error!void {
657 switch (c) {
658 '\\' => try writer.writeAll("\\\\"),
659 '\"' => try writer.writeAll("\\\""),
660 0x08 => try writer.writeAll("\\b"),
661 0x0C => try writer.writeAll("\\f"),
662 '\n' => try writer.writeAll("\\n"),
663 '\r' => try writer.writeAll("\\r"),
664 '\t' => try writer.writeAll("\\t"),
665 else => try outputUnicodeEscape(c, writer),
666 }
667}
668
669/// Write `string` to `writer` as a JSON encoded string.
670pub fn encodeJsonString(string: []const u8, options: Options, writer: *Writer) Error!void {
671 try writer.writeByte('\"');
672 try encodeJsonStringChars(string, options, writer);
673 try writer.writeByte('\"');
674}
675
676/// Write `chars` to `writer` as JSON encoded string characters.
677pub fn encodeJsonStringChars(chars: []const u8, options: Options, writer: *Writer) Error!void {
678 var write_cursor: usize = 0;
679 var i: usize = 0;
680 if (options.escape_unicode) {
681 while (i < chars.len) : (i += 1) {
682 switch (chars[i]) {
683 // normal ascii character
684 0x20...0x21, 0x23...0x5B, 0x5D...0x7E => {},
685 0x00...0x1F, '\\', '\"' => {
686 // Always must escape these.
687 try writer.writeAll(chars[write_cursor..i]);
688 try outputSpecialEscape(chars[i], writer);
689 write_cursor = i + 1;
690 },
691 0x7F...0xFF => {
692 try writer.writeAll(chars[write_cursor..i]);
693 const ulen = std.unicode.utf8ByteSequenceLength(chars[i]) catch unreachable;
694 const codepoint = std.unicode.utf8Decode(chars[i..][0..ulen]) catch unreachable;
695 try outputUnicodeEscape(codepoint, writer);
696 i += ulen - 1;
697 write_cursor = i + 1;
698 },
699 }
700 }
701 } else {
702 while (i < chars.len) : (i += 1) {
703 switch (chars[i]) {
704 // normal bytes
705 0x20...0x21, 0x23...0x5B, 0x5D...0xFF => {},
706 0x00...0x1F, '\\', '\"' => {
707 // Always must escape these.
708 try writer.writeAll(chars[write_cursor..i]);
709 try outputSpecialEscape(chars[i], writer);
710 write_cursor = i + 1;
711 },
712 }
713 }
714 }
715 try writer.writeAll(chars[write_cursor..chars.len]);
716}
717
718test "json write stream" {
719 var out_buf: [1024]u8 = undefined;
720 var out: Writer = .fixed(&out_buf);
721 var w: Stringify = .{ .writer = &out, .options = .{ .whitespace = .indent_2 } };
722 try testBasicWriteStream(&w);
723}
724
725fn testBasicWriteStream(w: *Stringify) !void {
726 w.writer.end = 0;
727
728 try w.beginObject();
729
730 try w.objectField("object");
731 var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
732 defer arena_allocator.deinit();
733 try w.write(try getJsonObject(arena_allocator.allocator()));
734
735 try w.objectFieldRaw("\"string\"");
736 try w.write("This is a string");
737
738 try w.objectField("array");
739 try w.beginArray();
740 try w.write("Another string");
741 try w.write(@as(i32, 1));
742 try w.write(@as(f32, 3.5));
743 try w.endArray();
744
745 try w.objectField("int");
746 try w.write(@as(i32, 10));
747
748 try w.objectField("float");
749 try w.write(@as(f32, 3.5));
750
751 try w.endObject();
752
753 const expected =
754 \\{
755 \\ "object": {
756 \\ "one": 1,
757 \\ "two": 2
758 \\ },
759 \\ "string": "This is a string",
760 \\ "array": [
761 \\ "Another string",
762 \\ 1,
763 \\ 3.5
764 \\ ],
765 \\ "int": 10,
766 \\ "float": 3.5
767 \\}
768 ;
769 try std.testing.expectEqualStrings(expected, w.writer.buffered());
770}
771
772fn getJsonObject(allocator: std.mem.Allocator) !std.json.Value {
773 var v: std.json.Value = .{ .object = std.json.ObjectMap.init(allocator) };
774 try v.object.put("one", std.json.Value{ .integer = @as(i64, @intCast(1)) });
775 try v.object.put("two", std.json.Value{ .float = 2.0 });
776 return v;
777}
778
779test "stringify null optional fields" {
780 const MyStruct = struct {
781 optional: ?[]const u8 = null,
782 required: []const u8 = "something",
783 another_optional: ?[]const u8 = null,
784 another_required: []const u8 = "something else",
785 };
786 try testStringify(
787 \\{"optional":null,"required":"something","another_optional":null,"another_required":"something else"}
788 ,
789 MyStruct{},
790 .{},
791 );
792 try testStringify(
793 \\{"required":"something","another_required":"something else"}
794 ,
795 MyStruct{},
796 .{ .emit_null_optional_fields = false },
797 );
798}
799
800test "stringify basic types" {
801 try testStringify("false", false, .{});
802 try testStringify("true", true, .{});
803 try testStringify("null", @as(?u8, null), .{});
804 try testStringify("null", @as(?*u32, null), .{});
805 try testStringify("42", 42, .{});
806 try testStringify("42", 42.0, .{});
807 try testStringify("42", @as(u8, 42), .{});
808 try testStringify("42", @as(u128, 42), .{});
809 try testStringify("9999999999999999", 9999999999999999, .{});
810 try testStringify("42", @as(f32, 42), .{});
811 try testStringify("42", @as(f64, 42), .{});
812 try testStringify("\"ItBroke\"", @as(anyerror, error.ItBroke), .{});
813 try testStringify("\"ItBroke\"", error.ItBroke, .{});
814}
815
816test "stringify string" {
817 try testStringify("\"hello\"", "hello", .{});
818 try testStringify("\"with\\nescapes\\r\"", "with\nescapes\r", .{});
819 try testStringify("\"with\\nescapes\\r\"", "with\nescapes\r", .{ .escape_unicode = true });
820 try testStringify("\"with unicode\\u0001\"", "with unicode\u{1}", .{});
821 try testStringify("\"with unicode\\u0001\"", "with unicode\u{1}", .{ .escape_unicode = true });
822 try testStringify("\"with unicode\u{80}\"", "with unicode\u{80}", .{});
823 try testStringify("\"with unicode\\u0080\"", "with unicode\u{80}", .{ .escape_unicode = true });
824 try testStringify("\"with unicode\u{FF}\"", "with unicode\u{FF}", .{});
825 try testStringify("\"with unicode\\u00ff\"", "with unicode\u{FF}", .{ .escape_unicode = true });
826 try testStringify("\"with unicode\u{100}\"", "with unicode\u{100}", .{});
827 try testStringify("\"with unicode\\u0100\"", "with unicode\u{100}", .{ .escape_unicode = true });
828 try testStringify("\"with unicode\u{800}\"", "with unicode\u{800}", .{});
829 try testStringify("\"with unicode\\u0800\"", "with unicode\u{800}", .{ .escape_unicode = true });
830 try testStringify("\"with unicode\u{8000}\"", "with unicode\u{8000}", .{});
831 try testStringify("\"with unicode\\u8000\"", "with unicode\u{8000}", .{ .escape_unicode = true });
832 try testStringify("\"with unicode\u{D799}\"", "with unicode\u{D799}", .{});
833 try testStringify("\"with unicode\\ud799\"", "with unicode\u{D799}", .{ .escape_unicode = true });
834 try testStringify("\"with unicode\u{10000}\"", "with unicode\u{10000}", .{});
835 try testStringify("\"with unicode\\ud800\\udc00\"", "with unicode\u{10000}", .{ .escape_unicode = true });
836 try testStringify("\"with unicode\u{10FFFF}\"", "with unicode\u{10FFFF}", .{});
837 try testStringify("\"with unicode\\udbff\\udfff\"", "with unicode\u{10FFFF}", .{ .escape_unicode = true });
838}
839
840test "stringify many-item sentinel-terminated string" {
841 try testStringify("\"hello\"", @as([*:0]const u8, "hello"), .{});
842 try testStringify("\"with\\nescapes\\r\"", @as([*:0]const u8, "with\nescapes\r"), .{ .escape_unicode = true });
843 try testStringify("\"with unicode\\u0001\"", @as([*:0]const u8, "with unicode\u{1}"), .{ .escape_unicode = true });
844}
845
846test "stringify enums" {
847 const E = enum {
848 foo,
849 bar,
850 };
851 try testStringify("\"foo\"", E.foo, .{});
852 try testStringify("\"bar\"", E.bar, .{});
853}
854
855test "stringify non-exhaustive enum" {
856 const E = enum(u8) {
857 foo = 0,
858 _,
859 };
860 try testStringify("\"foo\"", E.foo, .{});
861 try testStringify("1", @as(E, @enumFromInt(1)), .{});
862}
863
864test "stringify enum literals" {
865 try testStringify("\"foo\"", .foo, .{});
866 try testStringify("\"bar\"", .bar, .{});
867}
868
869test "stringify tagged unions" {
870 const T = union(enum) {
871 nothing,
872 foo: u32,
873 bar: bool,
874 };
875 try testStringify("{\"nothing\":{}}", T{ .nothing = {} }, .{});
876 try testStringify("{\"foo\":42}", T{ .foo = 42 }, .{});
877 try testStringify("{\"bar\":true}", T{ .bar = true }, .{});
878}
879
880test "stringify struct" {
881 try testStringify("{\"foo\":42}", struct {
882 foo: u32,
883 }{ .foo = 42 }, .{});
884}
885
886test "emit_strings_as_arrays" {
887 // Should only affect string values, not object keys.
888 try testStringify("{\"foo\":\"bar\"}", .{ .foo = "bar" }, .{});
889 try testStringify("{\"foo\":[98,97,114]}", .{ .foo = "bar" }, .{ .emit_strings_as_arrays = true });
890 // Should *not* affect these types:
891 try testStringify("\"foo\"", @as(enum { foo, bar }, .foo), .{ .emit_strings_as_arrays = true });
892 try testStringify("\"ItBroke\"", error.ItBroke, .{ .emit_strings_as_arrays = true });
893 // Should work on these:
894 try testStringify("\"bar\"", @Vector(3, u8){ 'b', 'a', 'r' }, .{});
895 try testStringify("[98,97,114]", @Vector(3, u8){ 'b', 'a', 'r' }, .{ .emit_strings_as_arrays = true });
896 try testStringify("\"bar\"", [3]u8{ 'b', 'a', 'r' }, .{});
897 try testStringify("[98,97,114]", [3]u8{ 'b', 'a', 'r' }, .{ .emit_strings_as_arrays = true });
898}
899
900test "stringify struct with indentation" {
901 try testStringify(
902 \\{
903 \\ "foo": 42,
904 \\ "bar": [
905 \\ 1,
906 \\ 2,
907 \\ 3
908 \\ ]
909 \\}
910 ,
911 struct {
912 foo: u32,
913 bar: [3]u32,
914 }{
915 .foo = 42,
916 .bar = .{ 1, 2, 3 },
917 },
918 .{ .whitespace = .indent_4 },
919 );
920 try testStringify(
921 "{\n\t\"foo\": 42,\n\t\"bar\": [\n\t\t1,\n\t\t2,\n\t\t3\n\t]\n}",
922 struct {
923 foo: u32,
924 bar: [3]u32,
925 }{
926 .foo = 42,
927 .bar = .{ 1, 2, 3 },
928 },
929 .{ .whitespace = .indent_tab },
930 );
931 try testStringify(
932 \\{"foo":42,"bar":[1,2,3]}
933 ,
934 struct {
935 foo: u32,
936 bar: [3]u32,
937 }{
938 .foo = 42,
939 .bar = .{ 1, 2, 3 },
940 },
941 .{ .whitespace = .minified },
942 );
943}
944
945test "stringify array of structs" {
946 const MyStruct = struct {
947 foo: u32,
948 };
949 try testStringify("[{\"foo\":42},{\"foo\":100},{\"foo\":1000}]", [_]MyStruct{
950 MyStruct{ .foo = 42 },
951 MyStruct{ .foo = 100 },
952 MyStruct{ .foo = 1000 },
953 }, .{});
954}
955
956test "stringify struct with custom stringifier" {
957 try testStringify("[\"something special\",42]", struct {
958 foo: u32,
959 const Self = @This();
960 pub fn jsonStringify(v: @This(), jws: anytype) !void {
961 _ = v;
962 try jws.beginArray();
963 try jws.write("something special");
964 try jws.write(42);
965 try jws.endArray();
966 }
967 }{ .foo = 42 }, .{});
968}
969
970fn testStringify(expected: []const u8, v: anytype, options: Options) !void {
971 var buffer: [4096]u8 = undefined;
972 var w: Writer = .fixed(&buffer);
973 try value(v, options, &w);
974 try std.testing.expectEqualStrings(expected, w.buffered());
975}
976
977test "raw streaming" {
978 var out_buf: [1024]u8 = undefined;
979 var out: Writer = .fixed(&out_buf);
980
981 var w: Stringify = .{ .writer = &out, .options = .{ .whitespace = .indent_2 } };
982 try w.beginObject();
983 try w.beginObjectFieldRaw();
984 try w.writer.writeAll("\"long");
985 try w.writer.writeAll(" key\"");
986 w.endObjectFieldRaw();
987 try w.beginWriteRaw();
988 try w.writer.writeAll("\"long");
989 try w.writer.writeAll(" value\"");
990 w.endWriteRaw();
991 try w.endObject();
992
993 const expected =
994 \\{
995 \\ "long key": "long value"
996 \\}
997 ;
998 try std.testing.expectEqualStrings(expected, w.writer.buffered());
999}