Parsing means turning text into data.
For example, this text:
123can become the integer value:
123This text:
3.14can become a floating point value:
3.14This text:
truecan become a boolean value:
truePrograms parse text all the time. Command-line arguments are text. Environment variables are text. Configuration files often contain text. Network protocols often begin as text. Logs, CSV files, JSON files, and source code are all text formats.
Zig keeps parsing explicit. Text does not automatically become a number. You ask for a specific type, and parsing can fail.
Parsing Integers
Use std.fmt.parseInt to parse an integer.
const std = @import("std");
pub fn main() !void {
const text = "123";
const value = try std.fmt.parseInt(u32, text, 10);
std.debug.print("value = {}\n", .{value});
}Output:
value = 123This call has three important parts:
std.fmt.parseInt(u32, text, 10)u32 is the integer type you want.
text is the input byte slice.
10 is the base, also called the radix.
Base 10 means normal decimal numbers.
The Result Type Matters
This parses into u8:
const value = try std.fmt.parseInt(u8, "255", 10);That works because 255 fits inside u8.
This fails:
const value = try std.fmt.parseInt(u8, "256", 10);A u8 can store values from 0 to 255. The text "256" is too large.
Parsing checks this. Zig does not silently wrap the number.
Signed and Unsigned Integers
Unsigned integer types cannot store negative values.
const value = try std.fmt.parseInt(u32, "-1", 10);This fails because u32 cannot represent -1.
Use a signed type when negative values are valid:
const value = try std.fmt.parseInt(i32, "-1", 10);Now parsing succeeds.
Choose the type based on the meaning of the value.
A count might be usize.
An ID might be u64.
A temperature might be i32.
A small byte value might be u8.
Different Bases
The third argument to parseInt is the base.
Decimal:
const a = try std.fmt.parseInt(u32, "255", 10);Hexadecimal:
const b = try std.fmt.parseInt(u32, "ff", 16);Binary:
const c = try std.fmt.parseInt(u32, "11111111", 2);All three produce the number 255.
This is useful when working with file formats, byte flags, colors, permissions, and machine-level data.
Example:
const std = @import("std");
pub fn main() !void {
const dec = try std.fmt.parseInt(u32, "255", 10);
const hex = try std.fmt.parseInt(u32, "ff", 16);
const bin = try std.fmt.parseInt(u32, "11111111", 2);
std.debug.print("{} {} {}\n", .{ dec, hex, bin });
}Output:
255 255 255Handling Parse Errors
Parsing can fail.
The text might contain invalid characters:
12x3The number might be too large:
999999999999999999999999999999The text might be empty:
You can handle errors with catch.
const std = @import("std");
pub fn main() void {
const text = "12x3";
const value = std.fmt.parseInt(u32, text, 10) catch |err| {
std.debug.print("could not parse integer: {}\n", .{err});
return;
};
std.debug.print("value = {}\n", .{value});
}Output:
could not parse integer: error.InvalidCharacterIn real tools, you often print a clearer message:
std.debug.print("expected a decimal number, got {s}\n", .{text});The raw error is useful for debugging. A human message is better for users.
Parsing Floating Point Numbers
Use std.fmt.parseFloat for floating point values.
const std = @import("std");
pub fn main() !void {
const text = "3.14";
const value = try std.fmt.parseFloat(f64, text);
std.debug.print("value = {}\n", .{value});
}Output:
value = 3.14The first argument is the float type:
f32or:
f64Use f64 by default unless you have a reason to use f32.
Parsing Text with Spaces
Parsing usually expects the input slice to contain only the number.
This may fail:
const value = try std.fmt.parseInt(u32, " 123 ", 10);The spaces are part of the input.
Trim the text first:
const std = @import("std");
pub fn main() !void {
const raw = " 123 \n";
const text = std.mem.trim(u8, raw, " \n\r\t");
const value = try std.fmt.parseInt(u32, text, 10);
std.debug.print("value = {}\n", .{value});
}std.mem.trim removes matching bytes from both ends.
This call:
std.mem.trim(u8, raw, " \n\r\t")removes spaces, newlines, carriage returns, and tabs.
Splitting Text
Many text formats contain separators.
This line:
alice,30has two fields separated by a comma.
You can split it:
const std = @import("std");
pub fn main() !void {
const line = "alice,30";
var it = std.mem.splitScalar(u8, line, ',');
const name = it.next() orelse return error.InvalidInput;
const age_text = it.next() orelse return error.InvalidInput;
const age = try std.fmt.parseInt(u32, age_text, 10);
std.debug.print("name={s}, age={}\n", .{ name, age });
}Output:
name=alice, age=30The iterator gives one part at a time.
First call:
it.next()returns "alice".
Second call returns "30".
If a part is missing, we return an error.
Checking for Extra Fields
The input:
alice,30,adminhas an extra field.
Sometimes that should be an error.
if (it.next() != null) {
return error.InvalidInput;
}Now the parser accepts exactly two fields.
Complete example:
const std = @import("std");
pub fn main() !void {
const line = "alice,30";
var it = std.mem.splitScalar(u8, line, ',');
const name = it.next() orelse return error.InvalidInput;
const age_text = it.next() orelse return error.InvalidInput;
if (it.next() != null) {
return error.InvalidInput;
}
const age = try std.fmt.parseInt(u32, age_text, 10);
std.debug.print("name={s}, age={}\n", .{ name, age });
}This habit matters. A parser should define what it accepts and what it rejects.
Parsing Lines
A file often contains many lines.
alice,30
bob,25
charlie,40You can split by newline first, then parse each line.
const std = @import("std");
fn parseLine(line: []const u8) !void {
var it = std.mem.splitScalar(u8, line, ',');
const name = it.next() orelse return error.InvalidInput;
const age_text = it.next() orelse return error.InvalidInput;
if (it.next() != null) {
return error.InvalidInput;
}
const age = try std.fmt.parseInt(u32, age_text, 10);
std.debug.print("name={s}, age={}\n", .{ name, age });
}
pub fn main() !void {
const text =
\\alice,30
\\bob,25
\\charlie,40
;
var lines = std.mem.splitScalar(u8, text, '\n');
while (lines.next()) |line| {
if (line.len == 0) continue;
try parseLine(line);
}
}Output:
name=alice, age=30
name=bob, age=25
name=charlie, age=40This example uses a multiline string:
const text =
\\alice,30
\\bob,25
\\charlie,40
;Each line begins with \\.
Trimming Each Field
Real input often has spaces:
alice, 30The age field is:
30Trim it before parsing:
const age_clean = std.mem.trim(u8, age_text, " \t\r\n");
const age = try std.fmt.parseInt(u32, age_clean, 10);You may also trim the name:
const name_clean = std.mem.trim(u8, name, " \t\r\n");Full parser:
const std = @import("std");
fn parseLine(line: []const u8) !void {
var it = std.mem.splitScalar(u8, line, ',');
const raw_name = it.next() orelse return error.InvalidInput;
const raw_age = it.next() orelse return error.InvalidInput;
if (it.next() != null) {
return error.InvalidInput;
}
const name = std.mem.trim(u8, raw_name, " \t\r\n");
const age_text = std.mem.trim(u8, raw_age, " \t\r\n");
const age = try std.fmt.parseInt(u32, age_text, 10);
std.debug.print("name={s}, age={}\n", .{ name, age });
}
pub fn main() !void {
try parseLine("alice, 30");
}Parsing Booleans
The standard library has helpers for many kinds of parsing, but boolean parsing is simple enough to write directly.
const std = @import("std");
fn parseBool(text: []const u8) !bool {
if (std.mem.eql(u8, text, "true")) return true;
if (std.mem.eql(u8, text, "false")) return false;
return error.InvalidBoolean;
}
pub fn main() !void {
const value = try parseBool("true");
std.debug.print("{}\n", .{value});
}This function accepts exactly:
trueand:
falseIt rejects everything else.
That is a good parser design. Be clear about accepted input.
Parsing Key-Value Text
Many simple config formats look like this:
host=localhost
port=8080
debug=trueA line parser can split on =.
const std = @import("std");
fn parseBool(text: []const u8) !bool {
if (std.mem.eql(u8, text, "true")) return true;
if (std.mem.eql(u8, text, "false")) return false;
return error.InvalidBoolean;
}
pub fn main() !void {
const line = "port=8080";
var it = std.mem.splitScalar(u8, line, '=');
const key = it.next() orelse return error.InvalidInput;
const value_text = it.next() orelse return error.InvalidInput;
if (it.next() != null) {
return error.InvalidInput;
}
if (std.mem.eql(u8, key, "port")) {
const port = try std.fmt.parseInt(u16, value_text, 10);
std.debug.print("port = {}\n", .{port});
}
}This is not a full configuration parser. It is the beginning of one.
The core idea is simple:
split text
validate the number of fields
trim fields if needed
compare keys
parse values into specific types
Case Sensitivity
String comparison is usually case-sensitive.
std.mem.eql(u8, "true", "true")is true.
std.mem.eql(u8, "true", "True")is false.
That may be exactly what you want. Strict formats are easier to test and document.
If you want to accept multiple spellings, write that policy explicitly:
if (std.mem.eql(u8, text, "true")) return true;
if (std.mem.eql(u8, text, "True")) return true;
if (std.mem.eql(u8, text, "1")) return true;Do not let accepted input grow accidentally. A parser is part of your program’s interface.
Avoid Silent Defaults
A tempting mistake is to return a default value when parsing fails.
Bad idea:
fn parsePort(text: []const u8) u16 {
return std.fmt.parseInt(u16, text, 10) catch 8080;
}This hides bad input.
If the user writes:
port=eightythe program silently uses 8080.
That can be dangerous.
Prefer returning an error:
fn parsePort(text: []const u8) !u16 {
return std.fmt.parseInt(u16, text, 10);
}Then the caller can decide what to do.
Defaults are fine when they are intentional, but do not use defaults to hide parse failures.
A Small Complete Parser
Here is a small parser for this input:
name=alice
age=30
debug=trueIt fills this struct:
const Config = struct {
name: []const u8,
age: u32,
debug: bool,
};Full code:
const std = @import("std");
const Config = struct {
name: []const u8,
age: u32,
debug: bool,
};
fn parseBool(text: []const u8) !bool {
if (std.mem.eql(u8, text, "true")) return true;
if (std.mem.eql(u8, text, "false")) return false;
return error.InvalidBoolean;
}
fn parseConfig(text: []const u8) !Config {
var config = Config{
.name = "",
.age = 0,
.debug = false,
};
var lines = std.mem.splitScalar(u8, text, '\n');
while (lines.next()) |raw_line| {
const line = std.mem.trim(u8, raw_line, " \t\r\n");
if (line.len == 0) continue;
var parts = std.mem.splitScalar(u8, line, '=');
const raw_key = parts.next() orelse return error.InvalidInput;
const raw_value = parts.next() orelse return error.InvalidInput;
if (parts.next() != null) {
return error.InvalidInput;
}
const key = std.mem.trim(u8, raw_key, " \t\r\n");
const value = std.mem.trim(u8, raw_value, " \t\r\n");
if (std.mem.eql(u8, key, "name")) {
config.name = value;
} else if (std.mem.eql(u8, key, "age")) {
config.age = try std.fmt.parseInt(u32, value, 10);
} else if (std.mem.eql(u8, key, "debug")) {
config.debug = try parseBool(value);
} else {
return error.UnknownKey;
}
}
return config;
}
pub fn main() !void {
const text =
\\name=alice
\\age=30
\\debug=true
;
const config = try parseConfig(text);
std.debug.print("name={s}\n", .{config.name});
std.debug.print("age={}\n", .{config.age});
std.debug.print("debug={}\n", .{config.debug});
}Output:
name=alice
age=30
debug=trueThis parser is small, but it shows the right habits.
It trims whitespace.
It rejects malformed lines.
It rejects unknown keys.
It parses values into specific types.
It returns errors instead of guessing.
What You Should Remember
Parsing turns text into typed data.
std.fmt.parseInt parses integers.
std.fmt.parseFloat parses floating point numbers.
Parsing can fail, so use try or catch.
Choose the output type deliberately.
Use std.mem.trim to remove whitespace.
Use std.mem.splitScalar to split simple text formats.
Validate the number of fields.
Reject invalid input clearly.
Do not silently replace bad input with defaults.
Good parsing code is strict, explicit, and easy to test.