nom
A Rust parser combinator library, built for binary formats first, whose parsers return slices of their input instead of copying it and can ask for more data when input arrives in pieces.
- What it is
- Binary parsing and encoding (parser combinators)
- Type
- Open source
- Made by
- Open-source project created by Geoffroy Couprie, now in the rust-bakery organisation
- License
- MIT
- Facts checked
- 9 October 2026
What it is
In nom a parser is a function that takes input and returns IResult: either the remaining input plus the value it parsed, or an error. Small parsers come built in, such as tag("SN") to match literal bytes, take(4), be_u16 and le_u32 for integers in either byte order, and a bits module that treats bytes as a bit stream. Combinators build bigger parsers from them: pair and tuples run parsers in sequence, alt tries alternatives, many0 repeats, length_count reads a count and then that many items, and map_res converts the result. Since nom 8 (January 2025), combinators are applied through the Parser trait, written combinator(arg).parse(input).
Input can be &[u8], &str or bits, and when a parser returns part of its input it returns a slice of it rather than a copy. Each basic parser comes in a complete version, for input that is all in memory, and a streaming version that returns Incomplete with how many more bytes it needs, so a network service can wait for the next packet instead of failing. nom works without the standard library, with or without an allocator. Its README lists parsers built on it for TLS, X.509, DER, Kerberos, SNMP, pcap, Matroska, tar, gzip, SQL and more.
Why teams pick it
- Zero-copy: parsers hand back slices of the input, so a header parser can return the payload without copying it
- Streaming built in:
Incompletetells the caller how much more data is needed, and the same parser works on whole or chunked input - One set of combinators for bytes, bits and text, so it fits binary protocols and text formats such as configuration files alike
- Parsers are small functions that can be unit-tested alone; the README says fuzzing has so far found flaws only in code written outside nom
Watch out for
- Reads only: there is no matching serializer, so writing the format back out is separate code
- Three major rewrites of the API: functions replaced macros in 5.0 (2019), macros were removed in 7.0 (2021) and 8.0 (2025) moved to the
Parsertrait, so older tutorials don’t compile - You choose
streamingorcompletefor each basic parser; a streaming parser at the end of a whole file reportsIncompleterather than an error - Generic signatures can need hand-written type annotations, for example the error type when switching into bit-level parsing
How it fits with other technologies
What nom uses
A sensor packet with 4-bit fields and a counted array, parsed from complete and partial input · rust
use nom::{
bits::{bits, streaming::take},
bytes::streaming::tag,
error::Error,
multi::length_count,
number::streaming::{be_i16, be_u16, u8},
IResult, Parser,
};
#[derive(Debug)]
struct Reading {
version: u8,
flags: u8,
sensor_id: u16,
temps: Vec<i16>, // hundredths of a degree
}
// one byte split into two 4-bit fields
fn nibbles(input: &[u8]) -> IResult<&[u8], (u8, u8)> {
bits::<_, _, Error<(&[u8], usize)>, _, _>((take(4usize), take(4usize))).parse(input)
}
// "SN" | version: 4 bits | flags: 4 bits | sensor id: u16 | count: u8 | count × i16
fn reading(input: &[u8]) -> IResult<&[u8], Reading> {
let (input, _) = tag(&b"SN"[..]).parse(input)?;
let (input, (version, flags)) = nibbles(input)?;
let (input, sensor_id) = be_u16(input)?;
let (input, temps) = length_count(u8, be_i16).parse(input)?;
Ok((input, Reading { version, flags, sensor_id, temps }))
}
fn main() {
let bytes = [b'S', b'N', 0x12, 0x00, 0x07, 0x02, 0x08, 0x34, 0xFF, 0x9C, 0xAA];
println!("{:?}", reading(&bytes));
// Ok(([170], Reading { version: 1, flags: 2, sensor_id: 7, temps: [2100, -100] }))
// the second reading hasn't arrived yet: nom asks for more instead of failing
println!("{:?}", reading(&bytes[..8]));
// Err(Incomplete(Size(2)))
println!("{:?}", reading(b"XX\x12"));
// Err(Error(Error { input: [88, 88, 18], code: Tag }))
}See it in a real stack
No architecture uses nom yet. Drop it into the playground and wire it up yourself.Open playground →Where it came from2015–2025
Geoffroy Couprie wrote nom to parse binary formats in Rust as fast as the usual hand-written C parsers but without their buffer overflows. Couprie published it in early 2015, months before Rust itself reached 1.0, and nom’s own stable 1.0 followed that November. Its first parsers were written with Rust macros such as named! and do_parse!, which generated the code that consumed input safely.
In 2017 Couprie and Pierre Chifflier of France’s national cybersecurity agency, ANSSI, presented “Writing parsers like it is 2017” at the IEEE Security & Privacy workshops. They argued for fixing whole classes of parsing bugs by writing parsers in a memory-safe language with combinators, and showed nom parsers running inside C programs: an FLV demuxer for VLC and a TLS parser for the Suricata intrusion detection system.
Later versions changed how parsers are written while keeping the model. Version 5 (2019) rebuilt nom on plain functions, version 7 (2021) removed the original macros, and version 8 (2025) moved combinators onto a trait to shrink the generated code and the API, moving tools for programming-language parsing into a separate nom-language crate.
Why it’s built this way
- Return slices, never copies
- Rust’s compiler tracks which code owns which memory, and a slice carries a pointer and a length, so a parser can return parts of its input safely without copying them.
- Don’t own the input
- Because nom parsers only borrow the data, a host program in C can keep its own buffers and call into Rust, as the VLC and Suricata integrations did.
- Say “need more data” instead of guessing
- Network formats and huge files arrive in parts; nom reports
Incompleterather than returning a wrong result, so the outcome is the same whether data comes whole or in chunks. - From macros to functions to traits
- Macros came first; functions in 5.0 made parsers ordinary Rust, and traits in 8.0 cut the amount of code each parser generates.
Timeline
- 2015
First releases in early 2015; 1.0 in November
- 2017
“Writing parsers like it is 2017” puts nom parsers into VLC and Suricata
- 2019
5.0 rebuilds nom on functions instead of macros
- 2021
7.0 removes the original macro combinators
- 2025
8.0 moves to the
Parsertrait and splits outnom-language
- nom CHANGELOG (0.1.1 in 2015 to 8.0.0 in 2025)
- Chifflier and Couprie: Writing parsers like it is 2017 (IEEE S&P Workshops 2017)
- nom README (designed for binary formats from the beginning)
- GitHub: Geal (Geoffroy Couprie)
- Rust Blog: Announcing Rust 1.0 (May 15, 2015)
- crates.io API: nom versions and release dates