02

Parse, don't validate

#
  • pattern
  • established
  • fragment

A function returns a type that cannot be invalid by construction. Validation throws knowledge away; parsing keeps it in the type.

The everyday equivalent is connectors. A power plug does not fit a USB port not because someone inspects it at the port, but because the shape is different. Validation is a “do not insert here” sticker; parsing is a differently shaped connector. The first kind of error is caught at runtime, and only if nobody forgot the check; the second is never made at all. The same analogy carries over to the neighbouring cards: 3 — there are exactly as many connectors as there are meaningful variants, and an extra one cannot even be manufactured; LSP (1) — if the plug fits, the pins must carry the voltage promised on the label.

Code example (fragment)
fragment
Java
// record + compact constructor = smart constructor
public record Email(String value) {
    public Email {
        if (!value.matches(".+@.+\\..+")) throw new IllegalArgumentException(value);
    }
    public static Optional<Email> parse(String raw) {
        try { return Optional.of(new Email(raw)); }
        catch (IllegalArgumentException e) { return Optional.empty(); }
    }
}
// downstream, Email is always valid — no repeated checks
Rust
// newtype with a private field — the canonical form
pub struct Email(String);          // private field, can't be built from outside

impl Email {
    pub fn parse(raw: &str) -> Result<Self, EmailError> {
        raw.contains('@').then(|| Email(raw.to_owned())).ok_or(EmailError::Invalid)
    }
    pub fn as_str(&self) -> &str { &self.0 }
}
Go
// private field + constructor — the only way in
type Email struct{ value string }   // zero value is useless — on purpose

func ParseEmail(raw string) (Email, error) {
    if !strings.Contains(raw, "@") {
        return Email{}, fmt.Errorf("invalid email: %q", raw)
    }
    return Email{value: raw}, nil
}

func (e Email) String() string { return e.value }
Python
# frozen dataclass + factory; NewType for cheap cases
from dataclasses import dataclass

@dataclass(frozen=True, slots=True)
class Email:
    value: str

    def __post_init__(self):
        if "@" not in self.value:
            raise ValueError(f"invalid email: {self.value!r}")

# or pydantic: class Email(BaseModel): value: EmailStr — same thing, on steroids

Last technical review: 2026-08-04 · Reference · 112 principles