Skip to content

Serialization

res.encode writes a value as the response body, using a serializer's function that writes to a std.Io.Writer. We recommend json.zig for JSON and msgpack.zig for MessagePack. Both generate the code for your types at compile time.

Add them to the project:

$ zig fetch --save "git+https://github.com/lalinsky/json.zig#v0.2.0"
$ zig fetch --save "git+https://github.com/lalinsky/msgpack.zig#v0.10.0"

And import the modules in build.zig, the same way as Dusty:

const json = b.dependency("json", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("json", json.module("json"));

const msgpack = b.dependency("msgpack", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("msgpack", msgpack.module("msgpack"));

Encoding

This handler from examples/todo sends the list of to-dos as JSON:

fn api(app: *App, req: *http.Request, res: *http.Response) !void {
    const todos = try app.queryTodos(req.arena, "SELECT id, title, done FROM todos ORDER BY id", .{});
    try res.encode(.json, json.encode, todos);
}

The first argument is the content type, then the encode function, then the value. res.encode tells the function's parameters apart by their types. It passes the *std.Io.Writer for the body, the value, and a std.mem.Allocator if the function takes one. json.zig's and msgpack.zig's encode work as they are. So does serde.zig's toWriter.

If encoding fails, what it wrote is thrown away. The handler can still send something else.

Decoding

To decode a JSON body, pass it to json.zig's decodeFromSliceLeaky with req.arena as the allocator:

const UserInput = struct {
    name: []const u8,
    email: ?[]const u8 = null,
};

fn updateUser(id: http.Param("id", u64), req: *http.Request, res: *http.Response) !void {
    const body = try req.body() orelse "";
    const input = json.decodeFromSliceLeaky(UserInput, req.arena, body, .{}) catch |err| switch (err) {
        error.OutOfMemory => return err,
        else => {
            res.status = .bad_request;
            return res.print(.text, "Invalid JSON: {t}\n", .{err});
        },
    };
    const user = try saveUser(id.value, input);
    try res.encode(.json, json.encode, user);
}

A body that isn't valid JSON, or doesn't match the type, gets a 400 with the reason. Unknown fields are rejected by default. The json.zig documentation shows how to skip them.

JSON or MessagePack

A handler can pick the format from the request's Accept header:

fn getUser(
    id: http.Param("id", u64),
    accept: http.Header("Accept", ?[]const u8),
    res: *http.Response,
) !void {
    const user = try loadUser(id.value);
    const wants_msgpack = std.mem.indexOf(u8, accept.value orelse "", "msgpack") != null;
    if (wants_msgpack) {
        try res.encode(.msgpack, msgpack.encode, user);
    } else {
        try res.encode(.json, json.encode, user);
    }
}

.msgpack sends the body as application/vnd.msgpack.

std.json

res.json(value, .{}) and req.json(T) use std.json, so they work without another dependency. They also work with std.json.Value, which json.zig doesn't have. json.zig is much faster, especially at decoding. Unlike std.json, it leaves out optional fields that are null.

Next Steps

  • Requests covers reading the body in other ways.
  • Middleware covers code that runs around the handlers.