Custom Validation Error Handling

Whether you are using the default validators or have implemented custom ones, you can customize the behavior that occurs when validation fails, as well as create custom errors.

IValidationErrorHandler

Customize the validation fail behavior through the IValidationErrorHandler interface:

public interface IValidationErrorHandler<T extends IValidated> {

    /**
     * Handle a validation failure (i.e. a validation error not caused by an exception)
     *
     * @param source the entity that failed validation
     * @param action recommended action to take to resolve the error
     * @param reason human-readable cause of the error
     */
    void handleValidationFailure(T source, String action, String reason);

    /**
     * Handle a validation error (i.e. a validation error that is caused by an exception)
     *
     * @param source the entity that failed validation
     * @param action recommended action to take to resolve the error
     * @param ex the exception that caused the error
     */
    void handleValidationError(T source, String action, Exception ex);

    /**
     * Handle a validation error
     *
     * @param validationError the validation error to handle
     */
    void handleValidationError(IValidationError<T> validationError);

    /**
     * @return the list of validation errors
     */
    List<IValidationError<T>> getValidationErrors();
}

The default implementation of IValidationErrorHandler is DefaultValidationErrorHandler and is generic for all types of components that are validated. This default implementation collects all errors that occur during validation and a single exception is thrown in the postValidation() method of validator. The exception is returned as a ProblemDetail object that contains all the validation errors in its validationErrors property, including the line number for each error if the source is a file.

To implement a custom handler that logs errors as they occur and continues processing instead of throwing exceptions, you can do the following:

1. Create the Spring Bean

@Primary
@Component
@Slf4j
public class MyValidationErrorHandler<T> implements IValidationErrorHandler<T> {

    private final List<IValidationError<T>> validationErrors = new ArrayList<>();

    @Override
    public void handleValidationFailure(T source, String action, String reason) {
        validationErrors.add(new ValidationError<>(source, reason, action, null));
        log.error("Validation failed for " + source + ". Reason: " + reason);
    }

    @Override
    public void handleValidationError(T source, String action, Exception ex) {
        validationErrors.add(new ValidationError<>(source, ex.getMessage(), action, ex));
        log.error("Validation error for " + source, ex);
    }
    
    @Override
    public void handleValidationError(IValidationError<T> validationError) {
        validationErrors.add(validationError);
        log.error("Validation error for " + validationError.getSource() + ". Reason: " + validationError.getReason());
    }

    @Override
    public List<IValidationError<T>> getValidationErrors() {
        return validationErrors;
    }
}

note

Note the inclusion of the @Primary annotation, which tells Spring to use MyValidationErrorHandler instead of DefaultValidationErrorHandler.

2. Import the Spring Bean

Once the bean is created, you need to import it to the project. Once done, Spring will use the custom bean for handling validation errors.

IValidationError

In addition to customizing the behavior when a validation error occurs, you can also customize the validation errors themselves. The IValidationError interface allows you to define a custom validation error:

public interface IValidationError<T> {

  /**
   * @return the DTO object that was the source of the error
   */
  T getSource();

  /**
   * @return the human-readable cause of the error
   */
  String getReason();

  /**
   * @return the human-readable action to take to resolve the error
   */
  String getAction();

  /**
   * @return the underlying exception that caused the error
   */
  Throwable getException();
}

The default implementation of IValidationError is DefaultValidationError. This default is generic for all types of components that are validated, and uses Lombok’s @AllArgsConstructor and @Getter annotations to provide its functionality.

To create a custom validation error specific to incidents that had some structured formatting and default action, you can do the following:

public class MyIncidentValidationError implements IValidationError<IncidentDTO> {
    
    public static final String REASON_TEMPLATE = "Validation failed for Incident [%s] due to: %s";
    public static final String DEFAULT_ACTION = "Please check the incident details and try again.";

    protected IncidentDTO source;
    protected String action;
    protected String reason;
    protected Throwable exception;

    @Override
    public IncidentDTO getSource() {
        return source;
    }

    @Override
    public String getReason() {
        return String.format(REASON_TEMPLATE, source, reason);
    }

    @Override
    public String getAction() {
        return action == null ? DEFAULT_ACTION : action;
    }

    @Override
    public Throwable getException() {
        return exception;
    }
}

note

Note that IValidationError is not managed by Spring, so there is no need specify @Component or @Primary annotations. You can create as many custom implementations of IValidationError as you need.