javaspringvalidationfunctional-programmingvavr

Simplifying Validation rule sequence in Java


I have a list of validations on an object now these validation are quite expensive and involve an API call to another service. I need to short circuit the validation chain on the first time we receive a negative validation or a error/exception while calling the validation service. What I have currently, is each validation helper functions would return an Optional. This optional is empty when the validation is successful. If the validation failed either due to the response from the service called or an exception is thrown from the service, We just return that ValidationResult, at the end of the chain we return a Valid ValidationResult

This is the optional chaining here :

return checkBelongsToGroup(row)
                .orElseGet(() -> checkParentToGroup(row)
                        .orElseGet(() -> checkLocation(row)
                                .orElseGet(() -> builder()
                                .outcome(ValidationResult
                                        .Outcome.VALID).build())));



Now providing one of the validation functions to give a taste on how it looks:

public Optional<ValidationResult> checkBelongsToGroup(final Row row) {
        if (Strings.isNullOrEmpty(row.getPreviousId())) {
            return Optional.empty(); //valid
        }
        return Try.of(() -> serviceClient.getassociation(row))
                .toEither()
                .mapLeft(this::getThrowableError)
// helper function to change exception to ValidationResult
                .fold(validationError ->  Optional.of(validationError), getAssociationOutput -> {
                    final Association association = getAssociationOutput.getAssociation();
                    if (association != null && association.isActive()) {
                        return Optional.empty(); // Success case
                    } else {
                        return Optional.of(builder()
                                .outcome(ValidationResult.Outcome.INVALID)
                                .errorStringId(INVALID)
                                .build()); // Validation failed.
                    }
                });

    }


The weird part here is that Optional.empty is being used to resemble success case which is semantically wrong. Is there a way to do this kind of chained shortcircuited validation that goes on till the first exception of validation failed. I was thinking of Validation library from vavr for this but need some help.


Solution

  • The Optional.empty() seems like an ok idea, but I find the chaining of validation a bit confuse. In this case, maybe a typical series of conditional would be more readable. Something like:

    var result = checkBelongsToGroup(row);
    
    if (result.isEmpty()) {
        result = checkParentToGroup(row);
    }
    
    if (result.isEmpty()) {
        result = checkLocation(row);
    }
    
    if (!result.isEmpty()) {
        // manage error
    }
    
    // success code
    

    Another option can be to use a library like Vavr, or implement something similar. The result would look like (or similar, I do not remember the exact syntax):

    Validation<ValidationResult, Void> result = Validation.combine(
        checkBelongsToGroup(row),
        checkParentToGroup(row),
        checkLocation(row)
    ).ap((a, b, c) -> null);
    
    if (result.isInvalid()) {
        var failure = result.getError();
        // manage error
    }
    
    // success code
    

    A manual implementation, not tested, not much thinking, would maybe look like:

    Stream<Function<Row, ValidationResult>> validators = Stream.of(
        this::checkBelongsToGroup,
        this::checkParentToGroup,
        this::checkLocation
    );
    
    ValidationResult failure = validators
        .map(validator -> validator.apply(row))
        .filter(Optional::isPresent)
        .findFirst()
        .orElseGet(Optional::empty);
    
    if (failure.isPresent()) {
        // manage error
    }
    
    // success code