FV is a lightweight, type-safe, and functional validation library for Java 21+. It is designed with a focus on immutability, side-effect-free functions, and seamless integration with Vavr.
The library encourages "Validation at the Edge", ensuring that your domain objects (like Java Records) are always in a valid state by validating them during construction. But it can also be used to validate business rules later on.
To use FV in your project, add the following dependencies to your pom.xml:
<dependency>
<groupId>be.iffy.fv</groupId>
<artifactId>fv-core</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- Optional: predefined rules for common types -->
<dependency>
<groupId>be.iffy.fv</groupId>
<artifactId>fv-rules</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>fv-core: The core datatypes of the library. It contains theValidationfunctor, theRuleandMappingRuleinterfaces. It has minimal dependencies (primarily Vavr).fv-rules: A collection of reusableRuleinstances for common Java types (Strings, Integers, Collections, BigDecimals, etc.), allowing you to compose complex validations quickly.
A functional interface representing a check on a value of type T. Rules can be easily composed:
import static be.iffy.fv.rules.text.StringRules.strings;
Rule<String> myRule = strings().minLength(3)
.and(strings().contains("@").or(strings().contains("|")));An applicative functor that represents either a Valid value or an Invalid result containing one or more ErrorMessage objects. Unlike a standard Either, Validation accumulates all errors instead of stopping at the first one.
fv-core doesn't have rules by default, but the fv-rules modules has ships lots of pre-made, reusable rules for common types. You can also easli make your own rules using a construct like
Rule<String> notEmpty = Rule.of(s -> !s.isEmpty(), "string.cannot.be.empty");The dsl.fv.be.iffy.DSL class provides a readable way to define validations.
Use assertAllValid inside a Java Record constructor to ensure that only valid objects can ever be instantiated.
public record User(String username, int age) {
public User {
var values = assertAllValid(
validateThat(username, "username").map(String::trim).is(StringRules.minLength(3)),
validateThat(age, "age").is(IntegerRules.min(18))
);
this.username = values._1(); // Use the trimmed value from the validation chain
}
}If validation fails, a ValidationException is thrown, containing all accumulated errors.
If you prefer a pure functional approach without throwing exceptions:
Validation<User> userV = Validation.mapN(
validateThat(dto.name(), "name").is(strings().minLength(3)),
validateThat(dto.age(), "age").is(ints().min(18)),
User::new
);
if (userV.isValid()) {
User user = userV.get();
} else {
List<ErrorMessage> errors = userV.errors();
}Errors automatically track their location, which is useful for nested object structures:
Validation<Address> addressV = Validation.from(() -> new Address(dto.street()))
.at("address");
// If it fails, error messages will look like "address.street.cannot.be.blank"Tip: you can use the lombok FieldNameConstants annotation to have Lombok generate String constants for field names for you, so you can have
refactoring friendly and typesafe error messages when using at.
FV is built from the ground up to be functional:
- No Nulls:
nullvalues are treated as invalid by default in the DSL. - Immutability: All core types (
Validation,Rule,ErrorMessage) are immutable. - Vavr Powered: Uses Vavr's
List,Map,Tuple, andOptionfor a robust functional experience.
The project includes AssertJ integrations to make writing tests for your validations clean and expressive:
import static assertj.fv.be.iffy.ValidationAssert.assertThatValidation;
@Test
void validateUser_whenAgeIsTooLow_shouldHaveError() {
Validation<User> result = validateUser(invalidDto);
assertThatValidation(result)
.isInvalid()
.hasErrorMessages("age.too.young");
}This project is licensed under the Apache 2.0 License.