# Keats/validator: derive-based struct validation for Rust

> A Rust workspace that turns #[validate(...)] attributes into a Validate implementation, with nested struct and vector support. It fits HTTP request bodies and config structs, not schema-driven pipelines.

**Keats/validator** — Simple validation for Rust structs

- Repository: https://github.com/Keats/validator
- Stars: 2,520 · Forks: 189
- Language: Rust
- License: MIT
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/keats-validator

## What Keats/validator solves, and who actually needs it

Rust makes it easy to define a struct that represents incoming data and hard to check that data cheaply. The usual result is a block of if statements at the top of a handler: mail contains an at sign, site parses as a URL, age sits between two numbers. Keats/validator replaces that block with attributes on the fields themselves. The README describes it as a "Macros 1.1 custom derive to simplify struct validation inspired by marshmallow and Django validators", and the derive is the whole point: you write rules next to the field they constrain, and the crate generates the checking code.

The audience is narrow and specific. If you are building a web service in Rust and your request bodies are deserialized into structs with serde, this crate slots in directly. It also fits configuration structs and any domain type where the rules are known at compile time and depend only on the field values. It is a poor fit for rules that require a database lookup, a network call, or knowledge of the whole request context, because the built-in validators are pure functions over a single field.

## How the Validate derive turns attributes into a trait implementation

The crate is split into three workspace members: validator, validator_derive and validator_derive_tests. The derive crate reads the #[validate(...)] attributes at compile time and generates an impl of the Validate trait for your struct. That trait exposes a validate() method returning Result<(), ValidationErrors>, so the check is a normal method call rather than a macro invocation at the call site.

Errors are structured, not strings. ValidationErrorsKind is an enum with three variants: Struct(Box<ValidationErrors>) for a single nested struct, List(BTreeMap<usize, Box<ValidationErrors>>) for a vector of nested structs, and Field(Vec<ValidationError>) for ordinary fields. Each ValidationError carries a code, an optional message, and a params map. The README notes that the field's value is added to params automatically under the key value, which means your error renderer can echo the offending input without extra plumbing. For a vector of nested structs, the map is keyed on the index of the invalid entry, so you can point a client at preferences[2].name rather than the whole list.

Nesting is opt-in. A field is only validated as part of its parent when it is tagged with #[validate(nested)], and the child type must itself derive Validate. The README's second example shows a SignupData struct holding a ContactDetails and a Vec<Preference>, both tagged nested, plus an Option<bool> tagged #[validate(required)].

## Installing validator and validating your first struct

The README gives the dependency line directly. The derive support is behind a feature flag, so a build without features = ["derive"] gives you the validation functions and types but not the attribute macro.

```toml
[dependencies]
validator = { version = "0.20", features = ["derive"] }
```

With that in place, derive Validate alongside whatever serde derives you already use, and put attributes on the fields. This is the README's short example, trimmed to three fields.

```rust
use serde::Deserialize;
use validator::{Validate, ValidationError};

#[derive(Debug, Validate, Deserialize)]
struct SignupData {
    #[validate(email)]
    mail: String,
    #[validate(url)]
    site: String,
    #[validate(range(min = 18, max = 20))]
    age: u32,
}
```

Calling signup_data.validate() returns Ok(()) or a ValidationErrors value whose keys are the struct's field names. In this example every failure is a Field variant, so a handler can iterate the map and build a per-field error response. The README also shows a custom function validator, declared as custom(function = "validate_unique_username"), whose signature is fn(&str) -> Result<(), ValidationError> and which can return ValidationError::new("terrible_username") to signal a failure with a code you choose.

The built-in validators are worth reading before you write a custom one. email tests against the HTML5 regex, and the README is explicit that this marks some esoteric addresses invalid that would also fail in an email input. url takes no arguments. length accepts min, max or equal, with equal excluding the other two and causing a compilation error if combined, and a maximum of two arguments. range accepts min and max or the exclusive forms exclusive_min and exclusive_max, and the limits may be numeric literals or value paths such as "crate::MAX_CONSTANT". must_match compares two fields by name, for example #[validate(must_match(other = "password2"))], and errors if the named field is missing or has a different type. contains checks a substring or a hashmap key.

## Where the derive approach gets in your way

The attribute model is a compile-time contract, and that cuts both ways. Rules that need runtime context cannot be expressed as a built-in validator; you fall back to custom(function = "..."), and the README's signature takes the field value, not the surrounding struct or any external handle. A uniqueness check against a database therefore becomes a custom function with a global or thread-local connection, or a separate step after validate() returns. The crate does not hide that boundary, it just does not cross it.

Length and range arguments are integers and numbers, and the README warns about a specific failure: if you get an error saying the literal does not fit in i32, specify the number type in the literal directly, as in #[validate(range(max = 1000000000u64))]. That is a real papercut for large bounds.

The email validator is deliberately loose in the other direction. It follows the HTML5 regex, so addresses that are technically valid per RFC 5322 but rejected by browser email inputs will also be rejected here. If your users have such addresses, this validator is the wrong tool and you need a custom function.

One more constraint sits in the workspace manifest: rust-version is 1.88. On an older toolchain the build fails before any of this matters, and the README does not discuss a minimum supported Rust version, so the Cargo.toml is the place to check.

## How it compares with schema-first validation in another language

The nearest alternative in spirit is a schema-first validator, the kind you find in Node.js or Python ecosystems, where the rules are data rather than attributes: you declare a schema object and hand it a payload at runtime. The difference is where the rules live. With Keats/validator the rules are Rust attributes compiled into the binary, so a typo in a validator name or an argument count is a compile error, and there is no schema document to keep in sync with the types. With a schema-first library the rules are runtime data, which means they can be loaded from a file, shared with a frontend, or generated from an OpenAPI document, and a mistyped key fails at runtime instead.

That trade-off decides the choice. If your source of truth is a Rust struct and the rules are stable, attributes win because the compiler checks them. If your source of truth is a JSON Schema or a spec shared with other services, a schema-first validator fits better, and you would be fighting Keats/validator to express it. There is also a middle path the README mentions in passing: the crate can be used without the custom derive, since it exposes all the validation functions and types. That lets you call individual validators from hand-written code when an attribute does not fit, without pulling in a second library.

## Maintenance, licence and the cost of upgrading

The repository is not archived, and the last push was on 2026-07-27, which is recent enough that the project is not sitting idle. No releases are listed alongside the repository, so version history has to come from crates.io or CHANGELOG.md. The workspace declares resolver = "2" and rust-version = "1.88".

The licence is MIT, which is permissive and imposes no copyleft obligation on your own code. That is a statement about the licence identifier, not legal advice; if you redistribute the crate or ship a modified copy, read the LICENSE file in the repository root yourself.

Upgrade cost is dominated by the derive macro, not the runtime code. Because attributes are validated at compile time, a change to attribute syntax or argument rules surfaces as build failures across every annotated struct, which is noisy but safe: you cannot silently keep an old behaviour. The CHANGELOG.md at the repository root is the file to read before bumping the version, since the README documents current syntax only and does not describe deprecated forms or migration paths.

## Conclusion

Adopt Keats/validator when your input already arrives as a Rust struct and you want field errors keyed by field name without writing a validation layer by hand. Do not adopt it when the rules live in a JSON Schema or another language's spec, or when you need async or database-backed checks inside the attribute, since the built-in validators are pure functions over the field value. Before committing, check that your toolchain meets the workspace rust-version of 1.88, and confirm how you will map ValidationErrorsKind variants onto your API's error shape.

## FAQ

### How do I install Keats/validator in a Rust project?

Add it to your Cargo.toml dependencies with the derive feature enabled, as shown in the README: validator = { version = "0.20", features = ["derive"] }. The derive feature is what provides the Validate attribute macro.

### How do I use validator on a struct?

Derive Validate on the struct, put #[validate(...)] attributes on the fields you want checked, and call the generated validate() method, which returns Result<(), ValidationErrors>. You need to import the Validate trait for the method to be available.

### Can Keats/validator validate nested structs and vectors of structs?

Yes. Tag the field with #[validate(nested)] and make sure the child type also derives Validate. Errors from a single nested struct come back as a Struct variant, and errors from a vector come back as a List variant keyed on the index of the invalid entry.

### What does the email validator in Keats/validator accept?

It tests the string against the HTML5 regex, which the README says will mark some esoteric emails as invalid that would not be valid in an email input either. If you need broader acceptance, write a custom function validator instead.

### Can I use Keats/validator without the derive macro?

Yes. The README states the crate can be used without the custom derive because it exposes all the validation functions and types, so you can call them from hand-written code.

## Sources

- [Issues](https://github.com/Keats/validator/issues)
- [Keats/validator on GitHub](https://github.com/Keats/validator)
- [License: MIT](https://github.com/Keats/validator/blob/master/LICENSE)
- [README](https://github.com/Keats/validator/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/keats-validator
