Zig OBJ Parsing: A Small, Fast Parser You Can Verify
Parsing is reading bytes, splitting tokens, converting types, and verifying output. If the output is predictable, the parser is correct. I built a small OBJ vertex parser in Zig because Zig makes allocation and errors explicit, which forces you to understand what the parser is actually doing.
What the parser does
It reads a .obj file, extracts v vertex lines, parses the three floats, stores them in a dynamic list, and prints them back.
| File | Purpose |
|---|---|
data/sample.obj | Known test input |
src/main.zig | The parser |
Setup
mkdir zig-objcd zig-objzig init-exemkdir -p datacat > data/sample.obj <<'EOF'v 0.0 1.0 0.0v -1.0 0.0 0.0v 1.0 0.0 0.0EOF
The parser
const std = @import("std");const Vertex = struct {x: f32,y: f32,z: f32,};pub fn main() !void {var gpa = std.heap.GeneralPurposeAllocator(.{}){};defer _ = gpa.deinit();const allocator = gpa.allocator();var file = try std.fs.cwd().openFile("data/sample.obj", .{});defer file.close();const reader = file.reader();var buf_reader = std.io.bufferedReader(reader);var in_stream = buf_reader.reader();var vertices = std.ArrayList(Vertex).init(allocator);defer vertices.deinit();var line_buf: [512]u8 = undefined;while (try in_stream.readUntilDelimiterOrEof(&line_buf, '\n')) |line| {if (line.len == 0) continue;if (line[0] != 'v') continue;var it = std.mem.tokenize(u8, line, " ");_ = it.next(); // skip "v"const x = try std.fmt.parseFloat(f32, it.next().?);const y = try std.fmt.parseFloat(f32, it.next().?);const z = try std.fmt.parseFloat(f32, it.next().?);try vertices.append(.{ .x = x, .y = y, .z = z });}const out = std.io.getStdOut().writer();for (vertices.items) |v| {try out.print("v {d:.1} {d:.1} {d:.1}\n", .{ v.x, v.y, v.z });}}
Run it
zig build run
Expected output:
v 0.0 1.0 0.0v -1.0 0.0 0.0v 1.0 0.0 0.0
If the output matches the input exactly, the parser works for this input.
Why this is useful
This parser exercises the pieces that matter for low-level parsing: file I/O, buffered reading, tokenization, numeric conversion, dynamic allocation, and formatted output. Every allocation is explicit. Every failure is an error union. Zig does not let you ignore the hard parts, which is why it is a good teacher for parsing.
Common mistakes I hit:
- Forgetting the
tryon file open or parse calls. - Missing the defer to clean up the allocator.
- Assuming a line always starts with
vand not checkingline[0].
Closing
A parser is correct when input, logic, and output are all visible. This example is small, but the same loop applies to larger formats: read, tokenize, parse, store, verify.