Parse Dont Validate

parse-dont-validate

Summary

Validation that returns raw values forces callers to remember which invariants already hold. Parse uncertain input into types that carry those invariants, using constructors or standard parsing traits that honestly match the boundary.

Problem

Validation that returns the original raw value leaves the caller with a memory burden. They must remember which strings, maps, paths, or payloads have already passed which checks, and the compiler cannot tell trusted input from untrusted input.

Preferred Move

Parse uncertain input into a type that carries the invariant. Put trimming, normalization, decoding, and validation at the boundary where raw input enters the trusted part of the system. Downstream code should accept the parsed type, not the raw representation plus an expectation that validation already happened.

Use FromStr when parsing from text is the standard relationship. Use a named constructor when the boundary has domain policy, context, side effects, or multiple parse modes.

Tradeoff

Do not create wrapper types for one local branch that immediately unwraps them. Parsing types earn their keep when they remove repeated checks, clarify API contracts, or prevent invalid values from crossing a boundary.

Agent Instruction

When raw input is validated before use, prefer introducing a parsed type at that boundary. Report
which invariant the type now carries and which downstream checks were removed or made unnecessary.

Examples

Bad: validation returns (), so the raw string still has to be trusted by convention.

fn validate_slug(input: &str) -> Result<(), SlugError> {
    if input.is_empty() || input.contains('/') {
        return Err(SlugError::Invalid);
    }

    Ok(())
}

pub fn publish(slug: &str) -> Result<(), PublishError> {
    validate_slug(slug)?;
    repository::publish_by_slug(slug)
}

Good: parsing produces the value that trusted code accepts.

pub struct PatternSlug(String);

impl FromStr for PatternSlug {
    type Err = SlugError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        if input.is_empty() || input.contains('/') {
            return Err(SlugError::Invalid);
        }

        Ok(Self(input.to_owned()))
    }
}

pub fn publish(slug: PatternSlug) -> Result<(), PublishError> {
    repository::publish_by_slug(&slug)
}

References

Source Use Note
Parse, don't validate adapts Preserve learned input facts in the resulting type.
C-VALIDATE supports Enforce validity at the API boundary where practical.
FromStr adapts Text parsing should expose a typed result and parse error.