Cached at:
09/26/26, 09:29 PM
# Rusty thoughts on "Parse, don't validate"
Source: [https://eli.thegreenplace.net/2026/rusty-thoughts-on-parse-dont-validate](https://eli.thegreenplace.net/2026/rusty-thoughts-on-parse-dont-validate)
Like many programmers, I find Alexis King's[Parse, don't validate](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/)article fascinating, because it gives a name to an idiom that seems familiar and important \- one I've observed and used in the past without naming it explicitly\. This post is a review of the "Parse, don't validate" pattern applied to the Rust programming language \(the original post uses Haskell\)\. I was particularly interested in finding educational examples of this pattern in the Rust standard library and other well\-known projects\.
Without repeating the original article \(please read it first\!\), here's the gist of it\.
Consider the venerableVec; itsfirstmethod returnsOption<&T\>\. Why? Because a vector is not guaranteed to have any elements in it, so what to do iffirstis invoked on an empty one? Returning anOptionin this case is idiomatic in Rust[\[1\]](https://eli.thegreenplace.net/2026/rusty-thoughts-on-parse-dont-validate#footnote-1), with convenient syntax sugar for accepting the result of functions that returnOptionand deciding what to do next\.
So what's the issue?
Imagine we have a function to read some configuration paths from an env var, while enforcing the invariant that the list can't be empty:
```
use anyhow::{Result, ensure};
fn get_configuration_directories() -> Result<Vec<PathBuf>> {
let value = env::var("CONFIG_DIRS").context("could not read CONFIG_DIRS")?;
let directories: Vec<PathBuf> = value
.split(',')
.map(str::trim)
.map(PathBuf::from)
.collect();
ensure!(!directories.is_empty(), "empty CONFIG_DIRS");
Ok(directories)
}
```
So far, so good\. Now let's take a typical usage of this function:
```
fn main() -> Result<()> {
let config_dirs = get_configuration_directories()?;
match config_dirs.first() {
Some(cache_dir) => initialize_cache(cache_dir),
None => unreachable!("already checked that CONFIG_DIRS is non-empty"),
}
Ok(())
}
```
Onceget\_configuration\_directoriesreturns a successful result, we are guaranteed that the vector isn't empty\. And yet, if we want to get the first element of this vector, we have to use thefirstmethod that returnsOption<&T\>\. We are therefore forced \- again \- to handle a potentially empty case \(where the option isNone\)\.
As the original article states, this has a number of problems with code clarity, potential performance implications and a ticking time bomb if the invariant is ever changed inget\_configuration\_directories\.
The core issue is thatVecis fundamentally a type that can be empty; we can carry along a "This one can't be empty, pinky promise\!" comment on all the relevant code, but it's not formally checked by anything\.
## A type for "non\-empty" vector
The solution is leveraging the type system to enforce a newly established invariant\. We can use a separate type for "a vector that cannot be empty"; in fact, such types already exist in several Rust crates \- for example[nonempty](https://docs.rs/nonempty/latest/nonempty/):
```
pub struct NonEmpty<T> {
pub head: T,
pub tail: Vec<T>,
}
```
This type has no constructor that permits "no elements"; itsnewtakes one element, and itsfirstmethod returns&Twithout anOption:
```
pub const fn new(e: T) -> Self {
Self::singleton(e)
}
pub const fn singleton(head: T) -> Self {
NonEmpty {
head,
tail: Vec::new(),
}
}
pub const fn first(&self) -> &T {
&self.head
}
```
The rest of the crate deals with makingNonEmptybehave as close as possible to a normalVec, by implementing many useful traits, as well as conversions like:
```
pub fn from_vec(mut vec: Vec<T>) -> Option<NonEmpty<T>> {
if vec.is_empty() {
None
} else {
let head = vec.remove(0);
Some(NonEmpty { head, tail: vec })
}
}
```
Let's see how ourget\_configuration\_directoriesfunction would look if it returned aNonEmptyinstead of a plainVec:
```
fn get_configuration_directories() -> Result<NonEmpty<PathBuf>> {
let value = env::var("CONFIG_DIRS").context("could not read CONFIG_DIRS")?;
let directories = value
.split(',')
.map(str::trim)
.map(PathBuf::from)
.collect();
let Some(directories) = NonEmpty::from_vec(directories) else {
bail!("CONFIG_DIRS cannot be empty");
};
Ok(directories)
}
```
Note the use ofNonEmpty::from\_vechere \- this is where the invariant is established\. Now a successful result isNonEmpty, not justVec\. The client code looks like:
```
fn main() -> Result<()> {
let config_dirs = get_configuration_directories()?;
initialize_cache(config_dirs.first())?;
Ok(())
}
```
There's no need to check if the returned value is empty again; this is enforced by the type system\!
This is where the parse vs\. validate terminology of the original article comes from\. Whenget\_configuration\_directoriesreturned aVec, it simply validated it\. But when it returns aNonEmpty\- the vector is transformed into another entity which carries additional meaning\. If we treat the concept of parsing in the most generic sense \- "transforming data from one format to another", this fits\.
To mention a less artificial example, the[Rust rewrite of core POSIX utilities](https://github.com/rustcoreutils/posixutils-rs)usesNonEmptyin several places[\[2\]](https://eli.thegreenplace.net/2026/rusty-thoughts-on-parse-dont-validate#footnote-2)\. For example, when constructing a shell pipeline:
```
pub struct Pipeline {
pub commands: NonEmpty<Command>,
pub negate_status: bool,
}
```
The command parser's code:
```
fn parse_pipeline(&mut self, alias_table: &AliasTable) -> ParseResult<Option<Pipeline>> {
// pipeline = "!" command ("|" linebreak command)*
let negate_status = self.match_alternatives(&[CommandToken::Bang])?.is_some();
let mut commands = if let Some(command) = self.parse_command(alias_table)? {
NonEmpty::new(command)
} else {
return Ok(None);
};
// ...
```
A validPipelineis only returned if there are some commands in the parsed AST\. Otherwise, it just returnsNone\. Once this is done, the client code can usecommands\.first\(\)without having to worry about the possibility of it returningNone\.
## Gradual parsing and type refinement
A somewhat more interesting example can be found in the source code of[rust\-analyzer](https://github.com/rust-lang/rust-analyzer)\. This project has a type that represents an absolute filesystem path:
```
pub struct AbsPathBuf(Utf8PathBuf);
```
Instead of carrying around a regular path, the absoluteness is recorded in the type once the initial parsing and validation is done:
```
impl TryFrom<Utf8PathBuf> for AbsPathBuf {
type Error = Utf8PathBuf;
fn try_from(path_buf: Utf8PathBuf) -> Result<AbsPathBuf, Utf8PathBuf> {
if !path_buf.is_absolute() {
return Err(path_buf);
}
Ok(AbsPathBuf(path_buf))
}
}
```
Subsequent code doesn't have to validate the the path is absolute\. The type enforces it\.
Note also thatAbsPathBufwrapsUtf8PathBuf, notPathBuf\.Utf8PathBufis itself a custom, "parsed" type refinement from the[camino crate](https://docs.rs/camino/latest/camino/)\. Regular paths in the Rust standard library aren't guaranteed to be valid UTF\-8, so they cannot be easily converted to aString\(which has to be valid UTF\-8 in Rust\);camino::Utf8PathBufestablishes validity on construction, and can then be converted to a string with just:
```
fn as_str(&self) -> &str {
...
}
```
So we have an example of gradual parsing and type refinement here:
```
std::path::PathBuf
|
| prove UTF-8
|
V
camino::Utf8PathBuf
|
| prove absolute
|
V
rust-analyzer's paths::AbsPathBuf
```
## Non\-zero integers
Rust has a generic type called[NonZero](https://doc.rust-lang.org/std/num/struct.NonZero.html), to describe unsigned numeric quantities that are known to be non\-zero\.
For example,thread::available\_parallelismis defined as:
```
pub fn available_parallelism() -> Result<NonZero<usize>>
```
If the call is successful, it returns aNonZero<usize\>, which is like a normalusizewith the restriction that it's not zero\. Client code doesn't have to keep checking whether the parallelism is 0 \- it's enshrined in the type system\.
Rust defines the[division operator](https://doc.rust-lang.org/std/primitive.u32.html#impl-Div%3CNonZero%3Cu32%3E%3E-for-u32)withNonZero<usize\>in the denominator as an operation that "cannot panic"\.
NonZerohas an additional advantage: zero is an invalid value for the type, so Rust can use the zero bit pattern to representNone\. Consequently,Option<NonZeroUsize\>is[guaranteed to have the same size](https://doc.rust-lang.org/std/option/index.html#representation)and alignment asNonZeroUsizeitself \(and asusize\)\. This can avoid the extra storage that anOption<usize\>would generally require\.
## Parsing JSON
A common example of the "parse, don't validate" idiom appears in deserializing data from a JSON string\. Rust'sserdecrate enables us to do the parsing, with validated decisions encoded into the type system, e\.g\.:
```
#[derive(Debug, Deserialize)]
struct Config {
name: String,
workers: NonZeroUsize,
mode: Mode,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
enum Mode {
Fast,
Safe,
}
```
And then later:
```
let input = r#"
{
"name": "compiler",
"workers": 4,
"mode": "fast"
}
"#;
let config: Config = serde_json::from_str(input)?;
```
There is a lot happening behind the scenes:
- The types of all fields are enforced \(e\.g\. "name" cannot be an array\)\.
- Themodeis validated to be one of the enum values ofMode\.
- workersis validated to be a non\-zero integer, because of theNonZeroUsizefield type\.
We take code like this for granted these days, but it's still a great example of the pattern discussed in this post\. Once the parser convertedmodeinto theModeenum, no further validation is required\.
In dynamic languages like Python and JS, the process is usually much more manual\. Python'sjson\.loadsgives us a dictionary, and it's up to the user to validate its contents\. Libraries like Pydantic permit an approach closer to Rust's, but they're not universally used\.
---
[\[1\]](https://eli.thegreenplace.net/2026/rusty-thoughts-on-parse-dont-validate#footnote-reference-1)Other languages \- like Go or Python \- have a runtime check that raises some sort of exception or panic whenlst\[0\]is accessed on an empty list or slice\.[\[2\]](https://eli.thegreenplace.net/2026/rusty-thoughts-on-parse-dont-validate#footnote-reference-2)The project implements its ownNonEmpty, without relying on thenonemptycrate, but all the points in this post apply\.
---
For comments, please send me[an email](mailto:
[email protected])\.