433 lines
10 KiB
Markdown
433 lines
10 KiB
Markdown
# std.json - JSON Parsing and Serialization
|
|
|
|
JSON RFC 8259 compliant parsing and stringification. In Zig 0.16, streaming/file examples should use `std.Io.Reader`, `std.Io.Writer`, `std.Io.Dir`, and an explicit `std.Io`.
|
|
|
|
Primary Zig 0.16 release-note source: https://ziglang.org/download/0.16.0/release-notes.html
|
|
|
|
## Table of Contents
|
|
- [Parsing JSON](#parsing-json)
|
|
- [Serializing to JSON](#serializing-to-json)
|
|
- [Dynamic Values](#dynamic-values)
|
|
- [Custom Serialization](#custom-serialization)
|
|
- [Streaming API](#streaming-api)
|
|
- [Common Patterns](#common-patterns)
|
|
|
|
## Parsing JSON
|
|
|
|
### Parse into Struct
|
|
|
|
```zig
|
|
const Config = struct {
|
|
name: []const u8,
|
|
port: u16,
|
|
enabled: bool = true, // default value for missing fields
|
|
};
|
|
|
|
const json_str =
|
|
\\{"name": "server", "port": 8080}
|
|
;
|
|
|
|
const parsed = try std.json.parseFromSlice(Config, allocator, json_str, .{});
|
|
defer parsed.deinit();
|
|
|
|
const config = parsed.value;
|
|
// config.name == "server"
|
|
// config.port == 8080
|
|
// config.enabled == true (default)
|
|
```
|
|
|
|
### ParseOptions
|
|
|
|
```zig
|
|
const parsed = try std.json.parseFromSlice(T, allocator, json_str, .{
|
|
// What to do with duplicate fields
|
|
.duplicate_field_behavior = .@"error", // .use_first, .use_last, .@"error" (default)
|
|
|
|
// Allow unknown fields (default: error)
|
|
.ignore_unknown_fields = true,
|
|
|
|
// Max string/number length (default: input length for slices)
|
|
.max_value_len = 4096,
|
|
|
|
// Parse numbers vs keep as strings
|
|
.parse_numbers = true, // default: true
|
|
});
|
|
```
|
|
|
|
### Supported Types
|
|
|
|
| Zig Type | JSON |
|
|
|----------|------|
|
|
| `bool` | `true`, `false` |
|
|
| `i32`, `u64`, etc. | number or string |
|
|
| `f32`, `f64` | number or string |
|
|
| `?T` | value or `null` |
|
|
| `[]const u8` | string |
|
|
| `[N]u8` | string (fixed length) |
|
|
| `[]T`, `[N]T` | array |
|
|
| `struct` | object |
|
|
| `union(enum)` | object with single field |
|
|
| `enum` | string |
|
|
| `std.json.Value` | any JSON value |
|
|
|
|
### Parse into Dynamic Value
|
|
|
|
Use `std.json.Value` when structure is unknown at compile time:
|
|
|
|
```zig
|
|
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, json_str, .{});
|
|
defer parsed.deinit();
|
|
|
|
const value = parsed.value;
|
|
switch (value) {
|
|
.object => |obj| {
|
|
if (obj.get("name")) |name| {
|
|
std.debug.print("name: {s}\n", .{name.string});
|
|
}
|
|
},
|
|
.array => |arr| {
|
|
for (arr.items) |item| { ... }
|
|
},
|
|
.string => |s| { ... },
|
|
.integer => |i| { ... },
|
|
.float => |f| { ... },
|
|
.bool => |b| { ... },
|
|
.null => { ... },
|
|
.number_string => |s| { ... }, // unparsed number
|
|
}
|
|
```
|
|
|
|
### Leaky Parsing (Arena Allocator)
|
|
|
|
When using an arena, skip the `Parsed` wrapper:
|
|
|
|
```zig
|
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
|
defer arena.deinit();
|
|
|
|
const config = try std.json.parseFromSliceLeaky(
|
|
Config,
|
|
arena.allocator(),
|
|
json_str,
|
|
.{},
|
|
);
|
|
// No deinit needed - arena handles cleanup
|
|
```
|
|
|
|
## Serializing to JSON
|
|
|
|
### Simple Serialization
|
|
|
|
```zig
|
|
const config = Config{ .name = "app", .port = 3000 };
|
|
|
|
// To allocated string
|
|
const json = try std.json.Stringify.valueAlloc(allocator, config, .{});
|
|
defer allocator.free(json);
|
|
// json == {"name":"app","port":3000}
|
|
|
|
// To writer
|
|
var buf: [4096]u8 = undefined;
|
|
var writer = std.Io.File.stdout().writer(io, &buf);
|
|
try std.json.Stringify.value(config, .{}, &writer.interface);
|
|
try writer.interface.flush();
|
|
```
|
|
|
|
### Stringify Options
|
|
|
|
```zig
|
|
try std.json.Stringify.value(data, .{
|
|
// Whitespace formatting
|
|
.whitespace = .minified, // default: no whitespace
|
|
// .whitespace = .indent_2, // 2-space indent
|
|
// .whitespace = .indent_4, // 4-space indent
|
|
// .whitespace = .indent_tab,
|
|
|
|
// Include null optional fields? (default: true)
|
|
.emit_null_optional_fields = false,
|
|
|
|
// Emit []u8 as array of numbers instead of string
|
|
.emit_strings_as_arrays = false,
|
|
|
|
// Escape non-ASCII unicode as \uXXXX
|
|
.escape_unicode = false,
|
|
|
|
// Large integers as strings for JS compatibility
|
|
.emit_nonportable_numbers_as_strings = false,
|
|
}, writer);
|
|
```
|
|
|
|
### Supported Types for Serialization
|
|
|
|
- `bool` → `true`/`false`
|
|
- `?T` → value or `null`
|
|
- integers → number (or string if > 2^53 with option)
|
|
- floats → number (or string if not precisely representable as f64)
|
|
- `[]const u8` → string (or array with option)
|
|
- `[]T`, `[N]T` → array
|
|
- tuples → array
|
|
- `struct` → object (fields in declaration order)
|
|
- `union(enum)` → object with one field
|
|
- `enum` → string
|
|
- `*T` → serialization of `T`
|
|
- `error` → string
|
|
|
|
## Dynamic Values
|
|
|
|
### Value Type
|
|
|
|
```zig
|
|
pub const Value = union(enum) {
|
|
null,
|
|
bool: bool,
|
|
integer: i64,
|
|
float: f64,
|
|
number_string: []const u8, // unparsed number
|
|
string: []const u8,
|
|
array: Array, // std.array_list.Managed(Value)
|
|
object: ObjectMap, // std.array_hash_map.String(Value)
|
|
};
|
|
```
|
|
|
|
### Building Values Manually
|
|
|
|
```zig
|
|
var obj: std.json.ObjectMap = .empty;
|
|
defer obj.deinit(allocator);
|
|
try obj.put(allocator, "name", .{ .string = "test" });
|
|
try obj.put(allocator, "count", .{ .integer = 42 });
|
|
|
|
var arr = std.json.Array.init(allocator);
|
|
try arr.append(.{ .integer = 1 });
|
|
try arr.append(.{ .integer = 2 });
|
|
try obj.put(allocator, "items", .{ .array = arr });
|
|
|
|
const value = std.json.Value{ .object = obj };
|
|
```
|
|
|
|
### Accessing Values
|
|
|
|
```zig
|
|
// Object access
|
|
if (value.object.get("key")) |v| {
|
|
switch (v) {
|
|
.string => |s| std.debug.print("{s}\n", .{s}),
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
// Array iteration
|
|
for (value.array.items) |item| {
|
|
if (item == .integer) {
|
|
std.debug.print("{d}\n", .{item.integer});
|
|
}
|
|
}
|
|
```
|
|
|
|
## Custom Serialization
|
|
|
|
### Custom jsonParse
|
|
|
|
Define `jsonParse` for custom deserialization:
|
|
|
|
```zig
|
|
const Point = struct {
|
|
x: i32,
|
|
y: i32,
|
|
|
|
// Parse from "x,y" string format
|
|
pub fn jsonParse(
|
|
allocator: std.mem.Allocator,
|
|
source: anytype,
|
|
options: std.json.ParseOptions,
|
|
) !@This() {
|
|
_ = allocator;
|
|
_ = options;
|
|
const token = try source.next();
|
|
const str = switch (token) {
|
|
.string, .allocated_string => |s| s,
|
|
else => return error.UnexpectedToken,
|
|
};
|
|
var it = std.mem.splitScalar(u8, str, ',');
|
|
return .{
|
|
.x = try std.fmt.parseInt(i32, it.next() orelse return error.UnexpectedToken, 10),
|
|
.y = try std.fmt.parseInt(i32, it.next() orelse return error.UnexpectedToken, 10),
|
|
};
|
|
}
|
|
};
|
|
|
|
// Parses: "10,20" → Point{ .x = 10, .y = 20 }
|
|
```
|
|
|
|
### Custom jsonStringify
|
|
|
|
Define `jsonStringify` for custom serialization:
|
|
|
|
```zig
|
|
const Point = struct {
|
|
x: i32,
|
|
y: i32,
|
|
|
|
pub fn jsonStringify(self: @This(), jw: anytype) !void {
|
|
// Serialize as "x,y" string
|
|
try jw.print("\"{d},{d}\"", .{ self.x, self.y });
|
|
}
|
|
};
|
|
|
|
// Serializes: Point{ .x = 10, .y = 20 } → "10,20"
|
|
```
|
|
|
|
## Streaming API
|
|
|
|
### Stringify (Write Stream)
|
|
|
|
Build JSON incrementally:
|
|
|
|
```zig
|
|
var out: std.Io.Writer.Allocating = .init(allocator);
|
|
defer out.deinit();
|
|
|
|
var jw: std.json.Stringify = .{
|
|
.writer = &out.writer,
|
|
.options = .{ .whitespace = .indent_2 },
|
|
};
|
|
|
|
try jw.beginObject();
|
|
try jw.objectField("users");
|
|
try jw.beginArray();
|
|
|
|
for (users) |user| {
|
|
try jw.beginObject();
|
|
try jw.objectField("name");
|
|
try jw.write(user.name);
|
|
try jw.objectField("age");
|
|
try jw.write(user.age);
|
|
try jw.endObject();
|
|
}
|
|
|
|
try jw.endArray();
|
|
try jw.endObject();
|
|
|
|
const json = out.written();
|
|
```
|
|
|
|
### Scanner (Low-Level Parsing)
|
|
|
|
Token-based parsing for streaming:
|
|
|
|
```zig
|
|
var scanner = std.json.Scanner.initCompleteInput(allocator, json_str);
|
|
defer scanner.deinit();
|
|
|
|
while (true) {
|
|
const token = try scanner.next();
|
|
switch (token) {
|
|
.object_begin => { ... },
|
|
.object_end => { ... },
|
|
.array_begin => { ... },
|
|
.array_end => { ... },
|
|
.string => |s| { ... },
|
|
.number => |n| { ... },
|
|
.true, .false, .null => { ... },
|
|
.end_of_document => break,
|
|
else => {},
|
|
}
|
|
}
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### Config File Loading
|
|
|
|
```zig
|
|
const Config = struct {
|
|
host: []const u8 = "localhost",
|
|
port: u16 = 8080,
|
|
debug: bool = false,
|
|
};
|
|
|
|
fn loadConfig(io: std.Io, allocator: std.mem.Allocator, path: []const u8) !Config {
|
|
const file = std.Io.Dir.cwd().openFile(io, path, .{}) catch |err| switch (err) {
|
|
error.FileNotFound => return Config{}, // defaults
|
|
else => return err,
|
|
};
|
|
defer file.close(io);
|
|
|
|
var read_buf: [4096]u8 = undefined;
|
|
var reader = file.reader(io, &read_buf);
|
|
const content = try reader.interface.allocRemaining(allocator, .limited(1024 * 1024));
|
|
defer allocator.free(content);
|
|
|
|
const parsed = try std.json.parseFromSlice(Config, allocator, content, .{
|
|
.ignore_unknown_fields = true,
|
|
});
|
|
defer parsed.deinit();
|
|
|
|
// Copy strings to owned memory since parsed and the source buffer are
|
|
// about to be freed. The caller owns and must free the returned host.
|
|
return Config{
|
|
.host = try allocator.dupe(u8, parsed.value.host),
|
|
.port = parsed.value.port,
|
|
.debug = parsed.value.debug,
|
|
};
|
|
}
|
|
```
|
|
|
|
### API Response Handling
|
|
|
|
```zig
|
|
const ApiResponse = struct {
|
|
success: bool,
|
|
data: ?Data = null,
|
|
@"error": ?[]const u8 = null, // use @"error" for reserved words
|
|
|
|
const Data = struct {
|
|
id: u64,
|
|
name: []const u8,
|
|
};
|
|
};
|
|
|
|
fn handleResponse(json: []const u8, allocator: std.mem.Allocator) !void {
|
|
const parsed = try std.json.parseFromSlice(ApiResponse, allocator, json, .{
|
|
.ignore_unknown_fields = true,
|
|
});
|
|
defer parsed.deinit();
|
|
|
|
if (!parsed.value.success) {
|
|
std.debug.print("Error: {s}\n", .{parsed.value.@"error" orelse "unknown"});
|
|
return error.ApiError;
|
|
}
|
|
|
|
if (parsed.value.data) |data| {
|
|
std.debug.print("Got: {s} (id={})\n", .{ data.name, data.id });
|
|
}
|
|
}
|
|
```
|
|
|
|
### Pretty Print JSON
|
|
|
|
```zig
|
|
fn prettyPrint(allocator: std.mem.Allocator, json: []const u8) ![]u8 {
|
|
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, json, .{});
|
|
defer parsed.deinit();
|
|
|
|
return std.json.Stringify.valueAlloc(allocator, parsed.value, .{
|
|
.whitespace = .indent_2,
|
|
});
|
|
}
|
|
```
|
|
|
|
### Serialize with Filtering
|
|
|
|
```zig
|
|
fn serializePublicFields(allocator: std.mem.Allocator, user: User) ![]u8 {
|
|
// Create anonymous struct with only public fields
|
|
const public = .{
|
|
.id = user.id,
|
|
.name = user.name,
|
|
// Exclude: .password, .internal_state
|
|
};
|
|
return std.json.Stringify.valueAlloc(allocator, public, .{});
|
|
}
|
|
```
|