Validation and Invariants
Rule
Validate untrusted data at input boundaries, convert it to invariant-bearing types, and let internal code operate on those trusted values.
Why
Early boundary validation keeps checks close to parsing and prevents repeated defensive validation throughout the system.
Do
- Validate CLI, HTTP, configuration, environment, database, message, and deserialization inputs.
- Convert raw values into domain types as soon as practical.
- Use fallible constructors or conversions for caller-provided values.
- Prevent construction paths that bypass the validated domain type.
- Use standard invariant types such as
NonZero*when they fit. - Fail startup cleanly when configuration is invalid.
- Re-check constraints that depend on mutable external state.
- Follow newtype and constructor policy for representation.
Avoid
- Do not validate the same invariant at every use site.
- Do not pass raw primitives deep into code when a validated type exists.
- Do not make
newpanic on caller input. - Do not treat deserialization alone as validation.
- Do not encode every invariant in typestate when a validated value is enough.
Library vs Application
Libraries prevent invalid public construction. Applications validate process and request inputs, then pass trusted types through internal layers.
Example
#![allow(unused)]
fn main() {
pub fn create_workspace(raw_name: &str) -> Result<Workspace, WorkspaceNameError> {
let name = WorkspaceName::try_new(raw_name)?;
Ok(Workspace::new(name))
}
pub fn workspace_path(root: &Path, name: &WorkspaceName) -> PathBuf {
root.join(name.as_str())
}
}
workspace_path trusts the invariant already established by WorkspaceName.
Exceptions
- Revalidate stale caches, legacy storage, and older serialized formats.
- Re-check authorization, uniqueness, quotas, existence, and time-dependent constraints.
- Use typestate when invalid transition order is important enough to reject at compile time.