How to make an HTTP POST request with struct serialization and default values

I have a local PostgREST instance on which I’m making HTTP requests in Zig v0.14.1.

I’m following the official documentation, at the end of which a simple todo list API gets created.

The schema of a todo is

{
    "id": integer,
    "done": boolean,
    "task": string,
    "due": string or null
},

A POST request can be made by:

curl http://localhost:3000/todos -X POST \
    -H "Authorization: Bearer $TOKEN"   \
    -H "Content-Type: application/json" \
    -d '{"task": "learn how to auth"}'

So the id is determined by the database and is auto-incremented, the default value of done is true, and due is null.


I made a todo struct with the following fields and method:

const Todo = struct {
    done: bool,
    task: []const u8,
    due: ?[]const u8,

    pub fn init() Todo {
        return .{
            .done = false,  // better default when a new task is being created
            .due = null,    // same as the curl request
        };
    }
};

I’d like to understand:

Is the init method the right way to deal with default values or should the fields be initialized to those values directly ?

and:

Since task is the only field required to make a POST request, a method to create a task in Todo can be along the lines of this one, in which the payload is passed directly (without serialization).

However, if a string was to be passed as a parameter

    ...
    ...
    pub fn create(task_name: []const u8) !void {

    }

how would a POST request look like on serializing the string to the struct?
(I’m not confident of the method’s signature)

I tried making the create method like:

this
    pub fn create(self: *Todo, gpa: Allocator, comptime token: []const u8, task_name: []const u8) !void {
        var client = http.Client{ .allocator = gpa };

        const endpoint = try std.Uri.parse("http://localhost:3000/todos");

        self.task = task_name;

        try json.stringify(self, .{}, out.writer());

        var buf: [1024]u8 = undefined;

        var request = try client.open(
            .POST,
            endpoint,
            .{
                .server_header_buffer = &buf,
                .headers = .{
                    .content_type = .{ .override = "application/json" },
                    .authorization = .{ .override = "Bearer: " ++ token },
                },
            },
        );

        //request.transfer_encoding = .{ .content_length = parsed_struct.len };
        try request.send();
        var wtr = request.writer();
        try wtr.writeAll(self);
        try request.finish();
        try request.wait();

        // info
        print("{d}\n", .{request.response.status});

        var iter = request.response.iterateHeaders();
        while (iter.next()) |header| {
            print("{s}:\t{s}\n", .{ header.name, header.value });
        }

        var response = request.reader();
        const body = try response.readAllAlloc(gpa, 1024 * 1024 * 4);
        print("{s}\n", .{body});
    }

but the call to writeAll errors with:

error: expected type '[]const u8', found '*post.Todo'
        try wtr.writeAll(self);

The serialised json is wherever out.writer() writes too, it doesnt magically change your pointer to Todo into a slice of json text, zig has a strict type system.

instead of whatever out is, use the writer to your request, which you already have as wtr.

The serialised json is wherever out.writer() writes too, it doesnt magically change your pointer to Todo into a slice of json text, zig has a strict type system.

I think I understand this

instead of whatever out is, use the writer to your request, which you already have as wtr.

But dont understand this - how should it be used. I tried this:

const payload = try json.stringify(self, .{}, wtr);
try wtr.writeAll(payload);

and what would be the value of request.transfer_encoding (since wtr is created after it) ?

Other than the Zig cookbook guide, I couldn’t find an example.

sorry, I should have checked how the request writer handles the encoding length, if you write more bytes it returns an error, else it decreases the encoding length to match what was written. That should probably be changed.

you’ll have to grab the serialised json before giving it to the request so you can set the encoding length.

you could give stringify a writer to an ArrayList or FixedBufferStream.Writer or you could use stringifyAlloc

I would recommend FixedBufferStream since you can reasonably assume a maximum size for your json.

I’m finding it tough …

I think the PostgREST example has a lot of overhead, so here’s a slight modification of the Zig cookbook post:

Modified Zig cookbook post
const Anything = struct {
    name: []const u8,
    author: ?[]const u8 = null,

    pub fn post(gpa: Allocator) !void {
        var client = http.Client{ .allocator = gpa };
        defer client.deinit();

        const uri = try std.Uri.parse("https://httpbin.org/anything");

        const payload =
            \\ {
            \\  "name": "zig-cookbook",
            \\  "author": "John"
            \\ }
        ;

        var buf: [1024]u8 = undefined;
        var req = try client.open(.POST, uri, .{ .server_header_buffer = &buf });
        defer req.deinit();

        req.transfer_encoding = .{ .content_length = payload.len };
        try req.send();
        var wtr = req.writer();
        try wtr.writeAll(payload);
        try req.finish();
        try req.wait();

        // Occasionally, httpbin might time out, so we disregard cases
        // where the response status is not okay.
        if (req.response.status != .ok) {
            return;
        }

        var rdr = req.reader();
        const body = try rdr.readAllAlloc(gpa, 1024 * 1024 * 4);
        defer gpa.free(body);

        print("Body:\n{s}\n", .{body});
    }
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer std.debug.assert(gpa.deinit() == .ok);
    const allocator = gpa.allocator();

    const post_anything = try Anything.post(allocator);
    _ = post_anything;
}

const std = @import("std");
const print = std.debug.print;
const http = std.http;

const Allocator = std.mem.Allocator;

and its modification based on this post:

Modified Zig cookbook post based on this post (doesn't work)
const Anything = struct {
    name: []const u8,
    author: ?[]const u8 = null,

    pub fn init() Anything {
        return .{
            .name = "foo",
        };
    }

    pub fn post(self: *Anything, gpa: Allocator, name: []const u8) !void {
        var client = http.Client{ .allocator = gpa };
        defer client.deinit();

        const uri = try std.Uri.parse("https://httpbin.org/anything");

        self.name = name;

        var fbs = std.io.fixedBufferStream(&self);
        const payload = try json.stringify(self, .{}, fbs.writer());

        var buf: [1024]u8 = undefined;
        var req = try client.open(.POST, uri, .{ .server_header_buffer = &buf });
        defer req.deinit();

        req.transfer_encoding = .{ .content_length = payload.len };
        try req.send();
        var wtr = req.writer();
        try wtr.writeAll(payload);
        try req.finish();
        try req.wait();

        // Occasionally, httpbin might time out, so we disregard cases
        // where the response status is not okay.
        if (req.response.status != .ok) {
            return;
        }

        var rdr = req.reader();
        const body = try rdr.readAllAlloc(gpa, 1024 * 1024 * 4);
        defer gpa.free(body);

        print("Body:\n{s}\n", .{body});
    }
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer std.debug.assert(gpa.deinit() == .ok);
    const allocator = gpa.allocator();

    var some_thing = Anything.init();
    try some_thing.post(allocator, "fubar");
}

const std = @import("std");
const print = std.debug.print;
const http = std.http;
const json = std.json;

const Allocator = std.mem.Allocator;

When I think of it, this is what I should have asked

fixedBufferStream can only take a slice of bytes, or something that can coerce to a slice of bytes, that will be where data is written to via its writer.

Zig code is quite readable, if you don’t understand what something does, or get an error you didn’t expect, you can most likely figure it out quite easily if you spend some time reading the source of what you’re calling.

Zig code is quite readable, if you don’t understand what something does, or get an error you didn’t expect, you can most likely figure it out quite easily if you spend some time reading the source of what you’re calling.

I find it overwhelming because looking at the source and learning from it is something I’m not familiar with.

I did get it to work:

const Anything = struct {
    name: []const u8,
    author: ?[]const u8 = null,

    pub fn init() Anything {
        return .{
            .name = "foo",
        };
    }

    pub fn post(self: *Anything, gpa: Allocator, name: []const u8) !void {
        var client = http.Client{ .allocator = gpa };
        defer client.deinit();

        const uri = try std.Uri.parse("https://httpbin.org/anything");

        self.name = name;
        var buf: [1024]u8 = undefined;

        var fbs = std.io.fixedBufferStream(&buf);
        try json.stringify(self, .{}, fbs.writer());

        var req = try client.open(.POST, uri, .{ .server_header_buffer = &buf });
        defer req.deinit();

        req.transfer_encoding = .{ .content_length = fbs.getWritten().len };
        try req.send();
        var wtr = req.writer();
        try wtr.writeAll(fbs.getWritten());
        try req.finish();
        try req.wait();

        // Occasionally, httpbin might time out, so we disregard cases
        // where the response status is not okay.
        if (req.response.status != .ok) {
            return;
        }

        var rdr = req.reader();
        const body = try rdr.readAllAlloc(gpa, 1024 * 1024 * 4);
        defer gpa.free(body);

        print("Body:\n{s}\n", .{body});
    }
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer std.debug.assert(gpa.deinit() == .ok);
    const allocator = gpa.allocator();

    var some_thing = Anything.init();
    try some_thing.post(allocator, "fubar");
}

const std = @import("std");
const print = std.debug.print;
const http = std.http;
const json = std.json;

const Allocator = std.mem.Allocator;