⚡ Zig Guide LiveUnofficialbut fully verified
✓ Zig 0.17.0-dev.2122+3e15e99e6What's newOn an older Zig?

Parsing and Encoding

const std = @import("std");
const expect = std.testing.expect;
const expectError = std.testing.expectError;
const expectEqualStrings = std.testing.expectEqualStrings;

test "parseInt" {
    try expect(try std.fmt.parseInt(i32, "-42", 10) == -42);

    // Base 0 reads the prefix like a Zig literal: 0x, 0o, 0b, or decimal.
    try expect(try std.fmt.parseInt(u32, "0xff", 0) == 255);
    try expect(try std.fmt.parseInt(u32, "0b1010", 0) == 10);

    // Underscore separators are accepted, like Zig literals.
    try expect(try std.fmt.parseInt(u32, "1_000_000", 10) == 1_000_000);
}

test "parse failures are errors, not zeros" {
    try expectError(error.InvalidCharacter, std.fmt.parseInt(u8, "12a", 10));
    // The target type bounds the parse: 300 does not fit in a u8.
    try expectError(error.Overflow, std.fmt.parseInt(u8, "300", 10));
}

test "parseFloat" {
    try expect(try std.fmt.parseFloat(f64, "3.25") == 3.25);
    try expect(try std.fmt.parseFloat(f64, "-1e-3") == -0.001);
    try expect(std.math.isInf(try std.fmt.parseFloat(f32, "inf")));
}

test "hex" {
    const bytes = [_]u8{ 0xde, 0xad, 0xbe, 0xef };

    // bytesToHex returns a fixed-size array: the length is known at
    // compile time from the input, so no allocator is needed.
    const hex = std.fmt.bytesToHex(bytes, .lower);
    try expectEqualStrings("deadbeef", &hex);

    var back: [4]u8 = undefined;
    _ = try std.fmt.hexToBytes(&back, "deadbeef");
    try expect(std.mem.eql(u8, &back, &bytes));
}

test "base64" {
    const codec = std.base64.standard;

    var enc_buf: [16]u8 = undefined;
    const encoded = codec.Encoder.encode(&enc_buf, "zig");
    try expectEqualStrings("emln", encoded);

    var dec_buf: [16]u8 = undefined;
    const n = try codec.Decoder.calcSizeForSlice(encoded);
    try codec.Decoder.decode(dec_buf[0..n], encoded);
    try expectEqualStrings("zig", dec_buf[0..n]);
}

test "url_safe base64 for tokens and file names" {
    // standard base64 emits + and /, which break URLs and paths.
    const bytes = [_]u8{ 0xfb, 0xff };
    var buf: [8]u8 = undefined;
    try expectEqualStrings("+/8=", std.base64.standard.Encoder.encode(&buf, &bytes));
    try expectEqualStrings("-_8=", std.base64.url_safe.Encoder.encode(&buf, &bytes));
}

Parsing is fallible, and says so

std.fmt.parseInt returns an error union, so a bad character or an out-of-range value is a value you handle, never a silent zero. The target type is the bound: parseInt(u8, "300", 10) is error.Overflow because 300 does not fit. Passing base 0 reads the prefix the way a Zig literal would: 0x, 0o, 0b, or decimal, underscores allowed.

The two errors are worth distinguishing when reporting to a user. error.InvalidCharacter means the text was not a number at all; error.Overflow means it was a number and the type is too small. A config parser that says “port must be a number” for 70000 is unhelpful, and the error already told you which it was.

The target type doing the bounds checking is the useful part. There is no separate range validation to write and no chance of it disagreeing with the field it feeds, because the field’s type is the range. Parsing into u16 for a port number rejects 70000 without a line of code.

parseFloat accepts the usual decimal and scientific forms plus inf and nan.

Both parse the whole string, so trailing whitespace or a stray newline is an error rather than being ignored. Trim first with std.mem.trim when the input comes from a file or a terminal, which is nearly always.

Hex

bytesToHex returns a fixed-size array, not a slice, because the output length is known from the input at compile time. That means no allocator and no failure path. hexToBytes goes the other way into a caller-provided buffer.

It takes a case argument, .lower or .upper, so there is no ambiguity about what a digest will look like. Decoding accepts either.

Because the return is an array rather than a slice, take a reference when passing it somewhere that wants []const u8. A temporary needs a const binding first, since otherwise there is nothing to point at.

For formatting bytes inline rather than converting them, {x} on a byte slice does the same job directly inside a print.

base64: pick the alphabet for the destination

std.base64.standard uses + and /, which are fine in a MIME body and wrong in a URL or a file name. std.base64.url_safe swaps them for - and _. Both expose an Encoder and a Decoder; decoding is fallible because the input might not be valid base64. Size the destination with Decoder.calcSizeForSlice before decoding.

CodecEmitsUse for
standardA-Za-z0-9+/MIME, email, data URIs
url_safeA-Za-z0-9-_URLs, file names, tokens
*_no_padabove, no =fixed-width IDs

Sizing is the step people skip, and it does not fail loudly if you guess. On the encoding side, Encoder.calcSize(n) gives the exact output length, which is 4 bytes for every 3 input bytes, rounded up and padded. On the decoding side, calcSizeForSlice inspects the padding to give the exact answer, and calcSizeUpperBound gives a safe over-estimate when you want to allocate before looking. Both return an error for input that cannot be valid base64 at all.

encodeWriter writes straight to a std.Io.Writer, so encoding a large payload into a response needs no intermediate buffer.

Base64 is not encryption and not compression. It makes arbitrary bytes safe to put in a text field, and it costs a third more space to do it.