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 u8
s 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.