JSON What is the best way to describe a "list of lists"

I’m deserializing some json which contains lists of strings, and I was wondering what would be the best way map that to a zig struct. My initial idea was to use std.ArrayListss, but that failed and I suspect it’s because std.ArrayList lacks some jsonParse* functions.

Example hidden because it's not super important
const std = @import("std");
// This doesn't work
const StringList = struct {
    list: std.ArrayList([]const u8),
};

test StringList {
    const input = (
        \\ { "list": ["abc", "123"] }
    );

    const parsed = try std.json.parseFromSlice(
        StringList,
        std.testing.allocator,
        input,
        .{},
    );
    defer parsed.deinit();

    try std.testing.expectEqualSlices(u8, "abc", parsed.value.list.items[0]);
}

The error message is somewhat opaque[1]:

error: error union with payload of opaque type ‘anyopaque’ not allowed

but that’s beside the point.

I’m currently using slices of strings instead ([][]const u8), but I’m wondering if there’s a better or more idiomatic choice here?


  1. :face_with_hand_over_mouth: ↩︎

1 Like

My first try would be to use []const []const u8, but so far I haven’t really used json a lot with Zig.

2 Likes

As @Sze said, the idiomatic choice here is []const []const u8.

However, if you really need an ArrayList, you could write a wrapper that implements jsonParse:

Example
test "ArrayList" {
    const input =
        \\{"items":["item1", "item2", "item3"]}
    ;
    const expected: []const []const u8 = &.{ "item1", "item2", "item3" };

    const parsed = try std.json.parseFromSlice(DataList, std.testing.allocator, input, .{});
    defer parsed.deinit();

    for (0..expected.len) |i| {
        try std.testing.expectEqualSlices(u8, expected[i], parsed.value.items.inner.items[i]);
    }
}

const std = @import("std");

fn ArrayListJson(T: type) type {
    return struct {
        inner: std.ArrayList(T),

        pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() {
            var list = std.ArrayList(T).init(allocator);
            errdefer list.deinit();

            if (try source.next() != .array_begin) {
                return error.UnexpectedToken;
            }

            while (true) {
                const token = try source.nextAlloc(allocator, options.allocate.?);
                switch (token) {
                    inline .string, .allocated_string => |s| try list.append(s),

                    .array_end => break,
                    else => {
                        std.debug.print("reached unreachable with token {s}\n", .{@tagName(token)});
                        unreachable;
                    },
                }
            }

            return .{ .inner = list };
        }
    };
}

const DataList = struct {
    items: ArrayListJson([]const u8),
};

However, I would advise against this approach.

2 Likes

Yeah I’m not looking to implement my own jsonParse* functions unless I have to, so it’s good to know I was on the right track!