073 — Validated Types (Parse, Don't Validate)
Tutorial
The Problem
"Parse, don't validate" (Lexi Lambda, 2019) is a foundational design principle: instead of checking a precondition and continuing with the raw value, parse the input into a type that structurally PROVES the precondition is satisfied. NonEmptyString cannot be empty by construction — its type is the proof. PositiveInt cannot be negative — the type system, not runtime checks, enforces this.
This pattern eliminates entire categories of defensive programming. If a function takes NonEmptyString, callers cannot accidentally pass an empty string — the compiler prevents it. Downstream code needs no re-validation. Applied in Rust, types like Email, PositiveInt, and BoundedString<1, 50> make invalid states literally unrepresentable in the program's type structure.
Used in nutype (a Rust derive macro for validated newtypes), validator, and custom domain types throughout production Rust codebases, this pattern is especially valuable in domain-driven design where the business rules should be embedded in types, not scattered as defensive checks throughout the logic.
🎯 Learning Outcomes
Option<T> or Result<T, E>Code Example
#![allow(clippy::all)]
// 073: Parse Don't Validate — Validated Types
// Approach 1: NonEmptyString
#[derive(Debug, Clone, PartialEq)]
struct NonEmptyString(String); // private field!
impl NonEmptyString {
fn new(s: &str) -> Option<Self> {
if s.is_empty() {
None
} else {
Some(NonEmptyString(s.to_string()))
}
}
fn as_str(&self) -> &str {
&self.0
}
fn len(&self) -> usize {
self.0.len() // always >= 1
}
}
// Approach 2: PositiveInt
#[derive(Debug, Clone, Copy, PartialEq)]
struct PositiveInt(u32); // private field, always > 0
impl PositiveInt {
fn new(n: i32) -> Option<Self> {
if n <= 0 {
None
} else {
Some(PositiveInt(n as u32))
}
}
fn value(&self) -> u32 {
self.0
}
fn add(self, other: Self) -> Self {
PositiveInt(self.0 + other.0) // sum of positives is positive
}
}
// Approach 3: Email
#[derive(Debug, Clone, PartialEq)]
struct Email(String);
impl Email {
fn new(s: &str) -> Result<Self, String> {
if !s.contains('@') {
Err("Missing @".into())
} else if s.len() < 3 {
Err("Too short".into())
} else {
Ok(Email(s.to_string()))
}
}
fn as_str(&self) -> &str {
&self.0
}
}
// Using validated types — no further validation needed
fn greet(name: &NonEmptyString) -> String {
format!("Hello, {}!", name.as_str())
}
fn double_positive(n: PositiveInt) -> u32 {
n.value() * 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_non_empty_string() {
assert!(NonEmptyString::new("").is_none());
assert!(NonEmptyString::new("hello").is_some());
let s = NonEmptyString::new("Alice").unwrap();
assert_eq!(s.as_str(), "Alice");
assert_eq!(s.len(), 5);
}
#[test]
fn test_positive_int() {
assert!(PositiveInt::new(0).is_none());
assert!(PositiveInt::new(-5).is_none());
let n = PositiveInt::new(42).unwrap();
assert_eq!(n.value(), 42);
assert_eq!(double_positive(n), 84);
}
#[test]
fn test_positive_add() {
let a = PositiveInt::new(3).unwrap();
let b = PositiveInt::new(4).unwrap();
assert_eq!(a.add(b).value(), 7);
}
#[test]
fn test_email() {
assert!(Email::new("bad").is_err());
assert!(Email::new("a@b.com").is_ok());
assert_eq!(Email::new("a@b.com").unwrap().as_str(), "a@b.com");
}
#[test]
fn test_greet() {
let name = NonEmptyString::new("Alice").unwrap();
assert_eq!(greet(&name), "Hello, Alice!");
}
}Key Differences
pub on the field: struct NonEmptyString(String). OCaml uses abstract types in a module signature to hide the representation.nutype crate**: Rust's nutype crate generates validated newtypes via derive macros. OCaml has no equivalent — modules are written manually.OCaml Approach
OCaml uses abstract types in modules to hide the internal representation:
module NonEmptyString : sig
type t
val of_string : string -> t option
val to_string : t -> string
end = struct
type t = string
let of_string s = if s = "" then None else Some s
let to_string s = s
end
The sig restricts the visible interface: the t type is abstract outside the module, so callers cannot construct a NonEmptyString.t directly — only through of_string. This is OCaml's equivalent of Rust's private fields.</p>
Full Source
#![allow(clippy::all)]
// 073: Parse Don't Validate — Validated Types
// Approach 1: NonEmptyString
#[derive(Debug, Clone, PartialEq)]
struct NonEmptyString(String); // private field!
impl NonEmptyString {
fn new(s: &str) -> Option<Self> {
if s.is_empty() {
None
} else {
Some(NonEmptyString(s.to_string()))
}
}
fn as_str(&self) -> &str {
&self.0
}
fn len(&self) -> usize {
self.0.len() // always >= 1
}
}
// Approach 2: PositiveInt
#[derive(Debug, Clone, Copy, PartialEq)]
struct PositiveInt(u32); // private field, always > 0
impl PositiveInt {
fn new(n: i32) -> Option<Self> {
if n <= 0 {
None
} else {
Some(PositiveInt(n as u32))
}
}
fn value(&self) -> u32 {
self.0
}
fn add(self, other: Self) -> Self {
PositiveInt(self.0 + other.0) // sum of positives is positive
}
}
// Approach 3: Email
#[derive(Debug, Clone, PartialEq)]
struct Email(String);
impl Email {
fn new(s: &str) -> Result<Self, String> {
if !s.contains('@') {
Err("Missing @".into())
} else if s.len() < 3 {
Err("Too short".into())
} else {
Ok(Email(s.to_string()))
}
}
fn as_str(&self) -> &str {
&self.0
}
}
// Using validated types — no further validation needed
fn greet(name: &NonEmptyString) -> String {
format!("Hello, {}!", name.as_str())
}
fn double_positive(n: PositiveInt) -> u32 {
n.value() * 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_non_empty_string() {
assert!(NonEmptyString::new("").is_none());
assert!(NonEmptyString::new("hello").is_some());
let s = NonEmptyString::new("Alice").unwrap();
assert_eq!(s.as_str(), "Alice");
assert_eq!(s.len(), 5);
}
#[test]
fn test_positive_int() {
assert!(PositiveInt::new(0).is_none());
assert!(PositiveInt::new(-5).is_none());
let n = PositiveInt::new(42).unwrap();
assert_eq!(n.value(), 42);
assert_eq!(double_positive(n), 84);
}
#[test]
fn test_positive_add() {
let a = PositiveInt::new(3).unwrap();
let b = PositiveInt::new(4).unwrap();
assert_eq!(a.add(b).value(), 7);
}
#[test]
fn test_email() {
assert!(Email::new("bad").is_err());
assert!(Email::new("a@b.com").is_ok());
assert_eq!(Email::new("a@b.com").unwrap().as_str(), "a@b.com");
}
#[test]
fn test_greet() {
let name = NonEmptyString::new("Alice").unwrap();
assert_eq!(greet(&name), "Hello, Alice!");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_non_empty_string() {
assert!(NonEmptyString::new("").is_none());
assert!(NonEmptyString::new("hello").is_some());
let s = NonEmptyString::new("Alice").unwrap();
assert_eq!(s.as_str(), "Alice");
assert_eq!(s.len(), 5);
}
#[test]
fn test_positive_int() {
assert!(PositiveInt::new(0).is_none());
assert!(PositiveInt::new(-5).is_none());
let n = PositiveInt::new(42).unwrap();
assert_eq!(n.value(), 42);
assert_eq!(double_positive(n), 84);
}
#[test]
fn test_positive_add() {
let a = PositiveInt::new(3).unwrap();
let b = PositiveInt::new(4).unwrap();
assert_eq!(a.add(b).value(), 7);
}
#[test]
fn test_email() {
assert!(Email::new("bad").is_err());
assert!(Email::new("a@b.com").is_ok());
assert_eq!(Email::new("a@b.com").unwrap().as_str(), "a@b.com");
}
#[test]
fn test_greet() {
let name = NonEmptyString::new("Alice").unwrap();
assert_eq!(greet(&name), "Hello, Alice!");
}
}
Deep Comparison
Core Insight
Instead of validating at every use site, validate once at construction and encode the invariant in the type system. A NonEmptyString can never be empty — no runtime checks needed downstream.
OCaml Approach
option or resultRust Approach
struct NonEmptyString(String) with private fieldResultComparison Table
| Feature | OCaml | Rust |
|---|---|---|
| Hide constructor | Module signature | Private field |
| Validate | create : string -> t option | fn new(s) -> Result<Self> |
| Access | Getter function | .as_str() / .value() |
| Guarantee | Type-level | Type-level |
Exercises
Email(String) with Email::new(s: &str) -> Option<Email> that validates the string contains exactly one @ and a non-empty domain. Use it in a send_email(to: Email, subject: NonEmptyString, body: &str) function.BoundedString<const MIN: usize, const MAX: usize>(String) using const generics. new validates MIN <= s.len() <= MAX.RangeValue<const MIN: i32, const MAX: i32>(i32) with const generics. Implement Add<RangeValue<MIN, MAX>> for RangeValue<MIN, MAX> that saturates at the bounds.