Type Design
On this page
Make invalid states unrepresentable. The type system and serde should enforce constraints at parse time, not at runtime.
Enums over strings for fixed value sets. Never use
Stringwhere the valid values are known. Use#[serde(rename_all = "snake_case")]enums. This gives serde-level validation and eliminates manual string matching:// Bad: mode: String, // "per_ip" | "global" // Good: #[derive(Deserialize)] #[serde(rename_all = "snake_case")] enum Mode { PerIp, Global }Structs over maps for known keys. Never use
BTreeMap/HashMapfor config deserialization when the key set is known. Use a struct with#[serde(deny_unknown_fields)]. Maps silently absorb unknown keys. Only use maps when the key set is genuinely open (e.g. user-defined header names).Enums over multiple
Option<T>fields. When exactly one of N fields must be set, use an N-variant enum. ThreeOptionfields with “exactly one must beSome” invariants should be a three-variant enum. Serde’s#[serde(rename_all = "snake_case")]with external tagging handles YAML naturally.#[serde(default)]overOption<T>withunwrap_or. If anOption<T>is always resolved with.unwrap_or(DEFAULT), use the concrete type with#[serde(default = "fn_name")]instead.#[serde(try_from)]for constrained numerics. When a numeric field only accepts specific values (e.g. HTTP redirect status 301/302/307/308), define an enum withTryFrom<u16>and#[serde(try_from = "u16")]. Validation moves to parse time.#[serde(deny_unknown_fields)]by default. Apply to all config structs unless the struct intentionally accepts arbitrary keys (extension points). Catches typos at parse time.