Data validation is a cornerstone of robust applications. It ensures that the data your application processes conforms to expected formats and business rules, preventing errors and maintaining data integrity.
If you're familiar with Django REST Framework (DRF) in Python, you'll appreciate its powerful and declarative validator system.
In this article, we'll explore how to build a type-safe and flexible validator system in Rust, taking cues from DRF's elegant design. We'll start with a basic structure and progressively enhance it to create a more practical and feature-rich solution.
Defining the Validators Struct
Let's begin by opening a base Rust structure for our validator system:
type InputDataType = u32;
struct Validators<T> where T : Fn(InputDataType) -> bool {
validators: Vec<T>,
data: InputDataType
}
impl <T>Validators<T> where T : Fn(InputDataType) -> bool {
fn new(validators: Vec<T>, data: InputDataType) -> Self {
Validators { validators, data }
}
fn validate(&self) -> bool {
self.validators.iter().all(|f| f(self.data))
}
}Let's break down this code piece by piece:
type InputDataType = u32;: This line defines a type aliasInputDataTypeasu32. This signifies the type of data we intend to validate. In a real-world scenario, this could be a struct, enum, like a JSON string or even a trait object, or any other complex data type. For simplicity in this example, we're using an unsigned 32-bit integer.struct Validators<T> where T : Fn(u32) -> bool { ... }: This is the heart of our validator system.struct Validators<T>: We define a generic struct namedValidators. The<T>indicates that Validators is generic over a type parameterT.where T : Fn(InputDataType) -> bool: This is a crucial constraint. It dictates that the typeTmust be a function (or a closure) that:Takes a
InputDataTypeas input matching our data field.Returns a
boolvalue, indicating whether the validation passed (true) or failed (false).
validators: Vec<T>: This field holds aVec(vector) of the validator functions of typeT. This is where we store our collection of validation rules.data: InputDataType: This field stores the actual data of typeInputDataTypethat we want to validate.
impl <T>Validators<T> where T : Fn(InputDataType) -> bool { ... }: This is the implementation block for our Validators struct. Again, we use generics and the same trait bound as in the struct definition.fn new(validators: Vec<T>, data: InputDataType) -> Self { ... }: This is a constructor function namednew.It takes a
Vec<T>of validator functions and theInputDataTypeas arguments.It returns
Self, which is shorthand forValidators<T>, creating a new instance of theValidatorsstruct with the provided values.
fn validate(&self) -> bool { ... }: This is the core validation logic.&self: It takes an immutable reference to theValidatorsinstance, allowing us to access its fields without taking ownership.-> bool: It returns aboolvalue indicating the overall validation result.self.validators.iter().all(|f| f(self.data)): This is the validation logic itself:self.validators.iter(): We iterate over thevalidatorsvector using.iter(), which provides immutable references to each validator function..all(|f| !f(self.data)): This is a higher-order function call..all()is a method on iterators that checks if all element in the iterator satisfies a given predicate (a boolean-returning function).|f| f(self.data): This is a closure (an anonymous function). For each validator functionfin thevalidatorsvector, it callsf(self.data). This executes the validator function with the input data..all()will returntrueif all the validator functions returntrue. If all validator functions returnfalse,.all()will returnfalse.
In essence, the current validate function checks if all of the provided validators pass for the given data. If all validators are satisfied, the overall validation is considered successful.
Enhancing the Validator System
While the core structure is functional, it has limitations and can be significantly improved to be more practical and aligned with the flexibility of DRF validators. Let's identify areas for enhancement and implement them:
Adding Validators Dynamically:
Currently, validators are provided only at the time of creating a
Validatorsinstance through thenewfunction. It would be more flexible to add validators incrementally after theValidatorsobject is created.Returning Descriptive Validation Errors:
The current
validatefunction simply returns abool. In real-world applications, it's crucial to know why validation failed. We need to return more informative error messages or structured error data.
Let's implement these enhancements step by step.
Adding Validators Dynamically
We can add a method add_validator to the Validators struct:
impl <T>Validators<T> where T : Fn(u32) -> bool {
// ... (new function and validate from before) ...
fn add_validator(&mut self, validator: T) {
self.validators.push(validator);
}
}This add_validator function takes a mutable reference to self (&mut self) and a new validator function of type T. It simply pushes the new validator to the validators vector using .push().
Now you can create a Validators instance and add validators to it later:
let mut validator_system: Validators<Box<dyn Fn(u32) -> bool>> = Validators::new(vec![], 15); // Start with no validators
validator_system.add_validator(Box::new(|data| data > 10));
validator_system.add_validator(Box::new(|data| data % 2 != 0)); // Odd number validator
let is_valid = validator_system.validate();
println!("Is valid (any): {}", is_valid); // Output: Is valid (any): true[!NOTE] We had to change the type of T in Validators to Box<dyn Fn(u32) -> bool>. This is because trait objects (like Fn(u32) -> bool) are not sized, and to store them in a Vec, we need to put them behind a pointer, in this case, a Box. Box<dyn Trait> is a common way to work with trait objects in Rust.
Returning Descriptive Validation Errors
Returning just a bool is limiting. Let's upgrade our system to return a Result that can carry error messages when validation fails.
First, let's define a custom error type (for simplicity, we'll use String for error messages, but in a real application, you'd likely create a more structured error enum or struct) probably using the thiserror crate:
type ValidationResult = Result<bool, String>;Now, let's modify our validate functions to return ValidationResult:
impl <T>Validators<T> where T : Fn(u32) -> bool {
// ... (new function and add_validator from before) ...
fn validate(&self) -> ValidationResult {
let mut errors = Vec::new();
for validator in &self.validators {
if !validator(self.data) {
// For a real system, you might want to collect error messages
errors.push("Validation failed for a rule".to_string());
}
}
if errors.is_empty() {
Ok(true) // All validators passed
} else {
Err(format!("Validation failed for all rules: {:?}", errors)) // Some validators failed
}
}
}In validate:
We iterate through all validators.
If a validator returns
false, we add a generic error message to anerrorsvector.If
errorsremains empty after checking all validators (meaning all passed), we returnOk(true).Otherwise, we return
Errcontaining a formatted error message (currently just listing generic messages, but you could enhance this to collect more specific errors).
Now, when you call validate, you get a Result that you can handle:
let mut validator_system: Validators<Box<dyn Fn(u32) -> bool>> = Validators::new(vec![], 15);
validator_system.add_validator(Box::new(|data| data > 10));
validator_system.add_validator(Box::new(|data| data % 2 != 0));
validator_system.add_validator(Box::new(|data| data < 15)); // This one will fail
let result = validator_system.validate();
match result {
Ok(_) => println!("Validation (any) passed!"),
Err(err) => println!("Validation (any) failed: {}", err),
} // Output: Validation (any) passed!Complete Code Example
Here's the complete enhanced code incorporating all the improvements:
type InputDataType = u32;
type ValidationResult = Result<bool, String>;
struct Validators<T> where T : Fn(u32) -> bool {
validators: Vec<T>,
data: InputDataType
}
impl <T>Validators<T> where T : Fn(u32) -> bool {
fn new(validators: Vec<T>, data: InputDataType) -> Self {
Validators { validators, data }
}
fn add_validator(&mut self, validator: T) {
self.validators.push(validator);
}
fn valiadte(&self) -> ValidationResult {
let mut errors = Vec::new();
for validator in &self.validators {
if !validator(self.data) {
// For a real system, you might want to collect error messages
errors.push("Validation failed for a rule".to_string());
}
}
if errors.is_empty() {
Ok(true) // All validators passed
} else {
Err(format!("Validation failed for all rules: {:?}", errors)) // Some validators failed
}
}
}
fn main() {
// Example Usage:
// Using Box<dyn Fn(u32) -> bool> to store closures in Vec
let mut validator_system: Validators<Box<dyn Fn(u32) -> bool>> = Validators::new(vec![], 15);
validator_system.add_validator(Box::new(|data| data > 10));
validator_system.add_validator(Box::new(|data| data % 2 != 0));
validator_system.add_validator(Box::new(|data| data < 20));
println!("Validating data: {}", validator_system.data);
let result = validator_system.validate();
match result {
Ok(_) => println!("Validation (all) passed!"),
Err(err) => println!("Validation (all) failed: {}", err),
}
}Conclusion
We've built a basic yet functional validator system in Rust inspired by the principles of Django REST Framework validators. It allows you to define a set of validation rules (as functions or closures) and apply them to your data. We enhanced the system to:
Dynamically add validators.
Return more informative validation
resultsusing Result and error messages.
In summary, you have just seen the core basics of how such a validation system might work in Rust. If you have any questions or you wish I write another article explaining how you could implement validators for your serde serialization, feel free to reach out to me on my linkedin.




