Table of Contents
Ok, ok. I must admit, there are a gazillion of validation frameworks out there. However, none of them really fits my needs. And here, is why:
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public Mono<UserDto> storeUser(@RequestBody UserDto userDto) {
return Validation
.of(userDto)
.with(Objects::nonNull)
.with(user -> Objects.nonNull(user.username()))
.with(user -> user.age() >= ADULT_LEGAL_AGE, user -> "The user is too young.")
.validate()
.map(userGateway::storeUser)
.orElseGet((result, errors) -> Mono.error(() -> new UserValidationException(errors)));
}
}As you can see, the validation definition is clear and concise. The functional approach lets you build up pipelines which are executed depending on the validation results.
Let's compare this lib to VAVR, which also contains a validation piece:
public class Vavr {
public void validate() {
PersonValidator personValidator = new PersonValidator();
// Valid(Person(John Doe, 30))
Validation<Seq<String>, Person> valid = personValidator.validatePerson("John Doe", 30);
// Invalid(List(Name contains invalid characters: '!4?', Age must be greater than 0))
Validation<Seq<String>, Person> invalid = personValidator.validatePerson("John? Doe!4", -1);
//A valid value is contained in a Validation.Valid instance, a list of validation errors is contained in a Validation.Invalid instance.
}
//The following validator is used to combine different validation results to one Validation instance.
class PersonValidator {
private static final String VALID_NAME_CHARS = "[a-zA-Z ]";
private static final int MIN_AGE = 0;
public Validation<Seq<String>, Person> validatePerson(String name, int age) {
return Validation.combine(validateName(name), validateAge(age)).ap(Person::new);
}
private Validation<String, String> validateName(String name) {
return CharSeq.of(name).replaceAll(VALID_NAME_CHARS, "").transform(seq -> seq.isEmpty()
? Validation.valid(name)
: Validation.invalid("Name contains invalid characters: '"
+ seq.distinct().sorted() + "'"));
}
private Validation<String, Integer> validateAge(int age) {
return age < MIN_AGE
? Validation.invalid("Age must be at least " + MIN_AGE)
: Validation.valid(age);
}
}
}public class SimFunValidation {
public void validate() {
var valid = new Person("John Doe", 30);
var invalid = new Person("John? Doe!4", -1);
var validValues = List.of(valid, invalid)
.stream()
.map(person -> Validation
.of(person)
.with(checkedPerson -> checkedPerson.getName().matches("[a-zA-Z ]+"), invalidPerson -> "Name contains invalid characters")
.with(checkedPerson -> MIN_AGE < checkedPerson.getAge(), invalidPerson -> "Age must be at least " + MIN_AGE)
.validate())
.filter(ValidationResult::isSuccessful)
.map(ValidationResult::get)
.collect(Collectors.toList());
}
}The project uses plain java without any other dependency pulled in. However, it uses Java 11 features (var) and thus can only be used in projects that use
Java 11 or above by themselves.
This lib uses Java 11 features. Thus, your project needs to be using at least Java 11. A backport is possible (replace var all over the place) - but hey, why?
Just declare a new dependency in your dependency management system as such:
<dependency>
<groupId>org.simfun</groupId>
<artifactId>validation</artifactId>
<version>1.0.0</version>
</dependency>dependencies { testImplementation "org.simfun:validation:1.0.0" }@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public Mono<UserDto> storeUser(@RequestBody UserDto userDto) {
return Validation
.of(userDto)
.with(Objects::nonNull)
.with(user -> Objects.nonNull(user.username()))
.with(user -> user.age() >= ADULT_LEGAL_AGE, user -> "The user is too young.")
.validate()
.map(userGateway::storeUser)
.orElseGet((result, errors) -> Mono.error(() -> new UserValidationException(errors)));
}
}@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public UserDto storeUser(@RequestBody UserDto userDto) {
var validationResult = Validation
.of(userDto)
.with(Objects::nonNull)
.with(user -> Objects.nonNull(user.username()))
.with(user -> user.age() >= ADULT_LEGAL_AGE)
.validate();
if (validationResult.isSuccessful()) {
return userGateway.storeUser(userDto);
}
throw new UserValidationException(validationResult.getMessages());
}
}The validation lib takes in any value, even null, as a value to be checked. However, it will reject null as value for a check and will also reject null
for an error message provider.
Allowed
var validation=Validation.of(null);Throws a NullPointerException
var badCheckUsage=Validation
.of("")
.with(null); //NPE thrown here due to check being null
var badMessageProviderUsage=Validation
.of("")
.with(valueToCheck->true,null); //NPE thrown here due to error message provider being null@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public Mono<UserDto> storeUser(@RequestBody UserDto userDto) {
return Validation
.of(user)
.with(Objects::nonNull)
.validate()
.flatMap(user -> Validation
.of(user.firstName)
.with(name -> Character.isLowerCase(name.charAt(0)), name -> String.format("[%s] doesn't start with a lower case letter", name))
.with(name -> Character.isUpperCase(name.charAt(name.length() - 1)), name -> String.format("[%s] doesn't end with an upper case letter", name))
.validate()
.map(firstName -> userDto))
.map(userGateway::storeUser)
.orElseGet((result, errors) -> Mono.error(() -> new UserValidationException(errors)));
}
}See the open issues for a list of proposed features (and known issues).
Contributions are what make the open source community such an amazing place to be, learn, inspire, and create. Any contributions you make are greatly appreciated.
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature) - Commit your Changes (
git commit -m 'Add some AmazingFeature') - Push to the Branch (
git push origin feature/AmazingFeature) - Open a Pull Request
Please find more information in CONTRIBUTING.md
Distributed under the MIT License. See LICENSE.md for more information.
Your Name - @lars_opitz
Project Link: https://github.com/lopitz/simfun-validation