Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- Support for Zig 0.17, alongside 0.16
- Decoding single-item pointers (`*T`, `?*T`), which makes recursive types work

### Fixed
- Structs with a few dozen fields failing to compile with "evaluation exceeded 1000 backwards branches"

## [0.1.0] - 2026-09-16

Expand Down
19 changes: 15 additions & 4 deletions src/decode.zig
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const Allocator = std.mem.Allocator;
const json = @import("json.zig");
const compat = @import("compat.zig");
const escapedLiteral = @import("encode.zig").escapedLiteral;
const fieldsBranchQuota = @import("encode.zig").fieldsBranchQuota;

pub const DecodeError = error{
/// A byte appeared where the grammar does not allow it.
Expand Down Expand Up @@ -419,10 +420,18 @@ pub const Decoder = struct {
}
return d.fixedArray(T, arr.child, arr.len);
},
.pointer => |ptr| {
if (ptr.size != .slice) @compileError("json: cannot decode " ++ @typeName(T));
if (ptr.child == u8) return d.string();
return d.slice(T, ptr.child);
.pointer => |ptr| switch (ptr.size) {
.slice => {
if (ptr.child == u8) return d.string();
return d.slice(T, ptr.child);
},
.one => {
const p = try d.gpa.create(ptr.child);
errdefer d.gpa.destroy(p);
p.* = try d.value(ptr.child);
return p;
},
else => @compileError("json: cannot decode " ++ @typeName(T)),
},
else => @compileError("json: cannot decode " ++ @typeName(T)),
}
Expand Down Expand Up @@ -959,6 +968,7 @@ pub const Decoder = struct {
comptime options: json.StructOptions,
comptime continuing: bool,
) DecodeError!T {
@setEvalBranchQuota(comptime fieldsBranchQuota(T));
const fields = comptime compat.structFields(T);

var result: T = undefined;
Expand Down Expand Up @@ -1224,6 +1234,7 @@ fn jsonKey(comptime T: type, comptime field_name: []const u8) []const u8 {

fn maxKeyLen(comptime T: type) usize {
comptime {
@setEvalBranchQuota(fieldsBranchQuota(T));
var m: usize = 1;
for (compat.structFields(T)) |f| m = @max(m, jsonKey(T, f.name).len);
return m;
Expand Down
16 changes: 16 additions & 0 deletions src/encode.zig
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ fn encodeUnion(comptime T: type, value: T, w: *Writer, opts: EncodeOptions) Enco
/// Writes a struct's members as `,"name":value`, for an object that already
/// has at least one member.
fn encodeStructFieldsAfter(comptime T: type, value: T, w: *Writer, opts: EncodeOptions) EncodeError!void {
@setEvalBranchQuota(comptime fieldsBranchQuota(T));
const options = comptime json.structOptions(T);
inline for (comptime compat.structFields(T)) |f| {
const omit = options.omit_null_fields and
Expand Down Expand Up @@ -189,6 +190,20 @@ pub fn escapedLiteral(comptime s: []const u8) []const u8 {
}
}

/// Inline loops over a struct's fields spend comptime branches on every field
/// (escaping its key, among other things), and all of it counts against the
/// enclosing function's quota. A struct with a few dozen fields runs past the
/// default of 1000, so functions looping over fields raise it to this.
pub fn fieldsBranchQuota(comptime T: type) u32 {
comptime {
const fields = compat.structFields(T);
@setEvalBranchQuota(1000 + 10 * fields.len);
var n: u32 = 1000;
for (fields) |f| n += 100 + 10 * @as(u32, @intCast(f.name.len));
return n;
}
}

fn fieldKey(comptime T: type, comptime field_name: []const u8) []const u8 {
const options = comptime json.structOptions(T);
return switch (options.key) {
Expand All @@ -209,6 +224,7 @@ fn canOmitFields(comptime T: type) bool {
}

fn encodeStruct(comptime T: type, value: T, w: *Writer, opts: EncodeOptions) EncodeError!void {
@setEvalBranchQuota(comptime fieldsBranchQuota(T));
const fields = comptime compat.structFields(T);
const options = comptime json.structOptions(T);

Expand Down
87 changes: 87 additions & 0 deletions src/test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,93 @@ test "nested structs, slices and fixed arrays" {
try testing.expectEqual(@as(usize, 0), v.empty.len);
}

test "single-item pointers, including recursive ones" {
const Node = struct {
value: u32,
next: ?*@This(),

pub fn jsonFormat() json.StructOptions {
return .{ .omit_null_fields = false };
}
};
const doc = "{\"value\":1,\"next\":{\"value\":2,\"next\":null}}";

var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
defer arena.deinit();
const v = try json.decodeFromSliceLeaky(Node, arena.allocator(), doc, .{});
try testing.expectEqual(@as(u32, 1), v.value);
try testing.expectEqual(@as(u32, 2), v.next.?.value);
try testing.expectEqual(@as(?*Node, null), v.next.?.next);
try expectEncodes(v, doc);

const p = try json.decodeFromSliceLeaky(*const u32, arena.allocator(), "7", .{});
try testing.expectEqual(@as(u32, 7), p.*);
}

test "a struct with many fields and a jsonFormat" {
// The per-field comptime work used to exhaust the default branch quota.
const Wide = struct {
field_number_0: u8 = 0,
field_number_1: u8 = 0,
field_number_2: u8 = 0,
field_number_3: u8 = 0,
field_number_4: u8 = 0,
field_number_5: u8 = 0,
field_number_6: u8 = 0,
field_number_7: u8 = 0,
field_number_8: u8 = 0,
field_number_9: u8 = 0,
field_number_10: u8 = 0,
field_number_11: u8 = 0,
field_number_12: u8 = 0,
field_number_13: u8 = 0,
field_number_14: u8 = 0,
field_number_15: u8 = 0,
field_number_16: u8 = 0,
field_number_17: u8 = 0,
field_number_18: u8 = 0,
field_number_19: u8 = 0,
field_number_20: u8 = 0,
field_number_21: u8 = 0,
field_number_22: u8 = 0,
field_number_23: u8 = 0,
field_number_24: u8 = 0,
field_number_25: u8 = 0,
field_number_26: u8 = 0,
field_number_27: u8 = 0,
field_number_28: u8 = 0,
field_number_29: u8 = 0,
field_number_30: u8 = 0,
field_number_31: u8 = 0,
field_number_32: u8 = 0,
field_number_33: u8 = 0,
field_number_34: u8 = 0,
field_number_35: u8 = 0,
field_number_36: u8 = 0,
field_number_37: u8 = 0,
field_number_38: u8 = 0,
field_number_39: u8 = 0,
field_number_40: u8 = 0,
field_number_41: u8 = 0,
field_number_42: u8 = 0,
field_number_43: u8 = 0,
field_number_44: u8 = 0,
field_number_45: u8 = 0,
field_number_46: u8 = 0,
field_number_47: u8 = 0,

pub fn jsonFormat() json.StructOptions {
return .{ .omit_null_fields = false };
}
};

var buf: [1024]u8 = undefined;
const doc = try encodeToBuf(Wide{ .field_number_47 = 9 }, &buf);
const v = try json.decodeFromSlice(Wide, std.testing.allocator, doc, .{});
defer v.deinit();
try testing.expectEqual(@as(u8, 9), v.value.field_number_47);
}

test "enums and tagged unions" {
const Color = enum { red, green, blue };
const Shape = union(enum) { circle: f64, rect: [2]u32, none };
Expand Down
Loading