Contact Info

sean [at] coreitpro [dot] com gpg key

Mastodon

sc68cal on Libera

Tokamak JSON serialization oddness

I’ve started poking around in the Zig programming language and I came across a strange issue.

I created a small function that pulls a row from sqlite and returns a struct.

fn regionDetail(conn: zqlite.Conn, id: i64) !Region {
    const query = "select * from Regions where RegionID=?";
    if (try conn.row(query, .{id})) |row| {
        defer row.deinit();
        return Region{
            .RegionID = row.int(0),
            .RegionDescription = row.text(1),
        };
    }
    return error.NotFound;
}

The tokamak library I am using, when you return a struct it will automatically serialize the struct into JSON.

What is strange however is that the serialization sometimes does not work correctly.

$ curl localhost:8000/Regions/2 
{"10"}
$ curl localhost:8000/Regions/2
{"RegionID":2,"RegionDescription":[88,245,26,21,0,0,0]}

There appears to be something going on where the response gets completely mangled and only the RegionID value is in the output, but it’s missing the key name. Other times the RegionDescription makes it through but it is just an array of u8s instead of being a JSON string.

I dug into the code in zqlite, tokamak and httpz. The zqlite library loads the RegionDescription column from the database as a C array of bytes (that are utf8 chars) from sqlite3_column_text, and just recasts it to []const u8. My code then returns a struct which tokamak then uses a httpz.Response’s json call which eventually works down to Zig’s std.json.stringify which apparently just turns it into an array of integers in JSON instead of recognizing that it should be turned into a string.

What’s strange is if I hack up the function a bit to just directly call the Response object’s json function myself, it serializes it correctly. Obviously this code is quite ugly but the point is to call json directly right where the result is.

fn regionDetail(res: *tk.Response, conn: zqlite.Conn, id: i64) !void {
    const query = "select * from Regions where RegionID=?";
    if (try conn.row(query, .{id})) |row| {
        defer row.deinit();
        const result = Region{
            .RegionID = row.int(0),
            .RegionDescription = row.text(1),
        };
        try res.json(result, .{});
        return;
    }
    return error.NotFound;
}
$ while true; do curl -s localhost:8000/Regions/2 | jq -c; done
{"RegionID":2,"RegionDescription":"Western"}
{"RegionID":2,"RegionDescription":"Western"}
{"RegionID":2,"RegionDescription":"Western"}

Between the odd JSON serialization failures where an invalid object would be created or the object would be created but RegionDescription would be the wrong type, I decided that maybe just creating a new string via std.fmt would maybe fix the issue and nudge std.json.stringify in the right direction.

fn regionDetail(alloc: std.mem.Allocator, conn: zqlite.Conn, id: i64) !Region {
    const query = "select * from Regions where RegionID=?";
    if (try conn.row(query, .{id})) |row| {
        defer row.deinit();
        return .{
            .RegionID = row.int(0),
            .RegionDescription = try std.fmt.allocPrint(
                alloc,
                "{s}",
                .{row.text(1)},
            ),
        };
    }
    return error.NotFound;
}
$ while true; do curl -s localhost:8000/Regions/2 | jq -c; done;
{"RegionID":2,"RegionDescription":"Western"}
{"RegionID":2,"RegionDescription":"Western"}
{"RegionID":2,"RegionDescription":"Western"}
{"RegionID":2,"RegionDescription":"Western"}
{"RegionID":2,"RegionDescription":"Western"}

I’m left scratching my head a bit. It has been a long time since I’ve used a programming language with manual memory allocation and this is my first attempt at playing around with Zig. Perhaps it’s a newbie mistake I’m making, but it does seem that something is amiss.