Usecases III: Validation

Let’s add another usecase to the multiplication example. One obvious additional feature would be to support division. The accompanying usecase is fairly trivial:

public final class DivisionUseCase {

    public CalculationResponse divide(final DivisionRequest divisionRequest) {
        final int divisor = divisionRequest.divisor;
        final int result = divisionRequest.dividend / divisor;
        return calculationResult(result);
    }
}

with the corresponding DivisionRequest:

public final class DivisionRequest {
    public final Integer dividend;
    public final Integer divisor;

    private DivisionRequest(final Integer dividend, final Integer divisor) {
        this.dividend = dividend;
        this.divisor = divisor;
    }

    public static DivisionRequest divisionRequest(final Integer dividend,
                                                  final Integer divisor) {
        return new DivisionRequest(dividend, divisor);
    }
}

To add this usecase to HttpMaid, all you need to do is add one single line:

final Gson GSON = new Gson();
final HttpMaid httpMaid = anHttpMaid()
        .post("/multiply", MultiplicationUseCase.class)
        .post("/divide", DivisionUseCase.class)
        .configured(toMarshallContentType(json(), string -> GSON.fromJson(string, Map.class), GSON::toJson))
        .build();

Since the DivisionRequest class resides in the same package as MultiplicationRequest, the MapMaid configuration from the previous example will auto-detect the new class and does not need to be changed.

You can try it out with the following curl command:

$ curl --request POST --header 'Content-Type: application/json' --data '{"dividend": "12", "divisor": "3"}' http://localhost:1337/divide

The output will be the correct result to the division of 12 by 3:

{"result":"4"}

Illegal input

Now, school taught every single one of us that there is one highly illegal thing you should never ever even think of doing: dividing by zero. Let’s go:

$ curl --request POST --header 'Content-Type: application/json' --data '{"dividend": "12", "divisor": "0"}' http://localhost:1337/divide

Unlike us, Java actually respects the laws of math and we should see the following exception on the console:

ERROR: java.lang.ArithmeticException: / by zero

with an accompanying stacktrace.

Obviously, our calculation application should be ready to deal with illegal input like this and handle it accordingly. As with every unhandled exception in HttpMaid, it is logged (to STDERR by default) and the request is answered with an empty status code 500 (Internal Server Error) response. This might not be particularly useful to the user, since a potential frontend needs to be able to tell what exactly was wrong about the provided input. Otherwise, the user would not know how to correct it.

Validating input

One way to tell the user what went wrong would be normal HttpMaid exception mapping. It has already been explained in previous chapters how this can be achieved. An obvious downside to this approach is that we need to execute the actual division to trigger the ArithmeticException and know that the input was wrong. By the time we execute the divsion, our input should already have been validated. This might not be obvious for the divison, but think of a more mature example, where the usecase would not consist of a simple math operation, but of database queries with potentially corrupt data. To mitigate this problem, we can validate the divisor in the DivisionRequest, so the exception would be thrown before the usecase and calculation could be called:

public final class DivisionRequest {
    public final Integer dividend;
    public final Integer divisor;

    public DivisionRequest(final Integer dividend, final Integer divisor) {
        this.dividend = dividend;
        this.divisor = divisor;
    }

    public static DivisionRequest divisionRequest(final Integer dividend,
                                                  final Integer divisor) {
        if (divisor == 0) {
            throw new IllegalArgumentException("the divisor must not be 0");
        }
        return new DivisionRequest(dividend, divisor);
    }
}

Let’s devide through zero again:

$ curl --request POST --header 'Content-Type: application/json' --data '{"dividend": "12", "divisor": "0"}' http://localhost:1337/divide

Now, we see an UnrecognizedExceptionOccurredException on the console. This is actually an exception of the MapMaid project, with our IllegalArgumentException down below the stacktrace as the causing exception:

Caused by: java.lang.IllegalArgumentException: the divisor must not be 0

This means that MapMaid is not prepared to see this particular exception i.e. it does not recognise it. In this case, MapMaid will abort and re-throw the exception wrapped in the observed UnrecognizedExceptionOccurredException since that is the only safe thing to do. However, if we tell MapMaid that an IllegalArgumentException is to be expected and that it indicates a failed validation, MapMaid will behave differently. Let’s tell MapMaid about the exception:

final HttpMaid httpMaid = anHttpMaid()
        .post("/multiply", MultiplicationUseCase.class)
        .post("/divide", DivisionUseCase.class)
        .configured(toMarshallContentType(json(), string -> GSON.fromJson(string, Map.class), GSON::toJson))
        .configured(toConfigureMapMaidUsingRecipe(mapMaidBuilder -> {
            mapMaidBuilder.withExceptionIndicatingValidationError(IllegalArgumentException.class);
        }))
        .build();

Now, when we devide by zero (again):

$ curl --request POST --header 'Content-Type: application/json' --data '{"dividend": "12", "divisor": "0"}' http://localhost:1337/divide

we actually get something meaningful out of it:

{"errors":[{"path":"","message":"the divisor must not be 0"}]}

MapMaid will gather all recognised validation exceptions and present all of them to us in a format that is easy to process.

Custom primitives

Let’s take a deeper look at MapMaid’s validation output:

{
   "errors":[
      {
         "path":"",
         "message":"the divisor must not be 0"
      }
   ]
}

Under the key error, we find a list of all occurred exceptions. Since only the DivisionRequest object has thrown one, the list has only one single entry. The entry has two fields. Under the key message, the message of the caught exception is stored. The key path is of more interest. Under it, MapMaid stores the logical location where the exception occured. This can be used to easily highlight the input fields in a user form that have been filled in incorrectly. Since the validation exception is thrown in the top level class DivisionRequest, the path is empty. Ideally, we want it to contain the value divisor, since this is the field that is validated. In order to achieve this, the IllegalArgumentException needs to be thrown in this exact field. Currently, it is thrown in the DivisionRequest class. In order for it to be located in the divisor field, we need to throw the exception in the class of this field. Unfortunately, the divisor’s data type is Integer and we cannot change the implementation of this class. To solve the problem, we need to write our own Divisor data type that contains the validation:

public final class Divisor {
    private final int value;

    private Divisor(final int value) {
        this.value = value;
    }

    public static Divisor parseDivisor(final String divisorAsString) {
        final int value = parseInt(divisorAsString);
        if (value == 0) {
            throw new IllegalArgumentException("the divisor must not be 0");
        }
        return new Divisor(value);
    }

    public int value() {
        return value;
    }

    public String stringValue() {
        return String.valueOf(value);
    }
}

We will call these kinds of classes custom primitives throughout this guide since they act pretty much the same as primitive data types like int, double, or even String (which is technically not a primitive data type but it is used like one). In the world of Domain-Driven Design (DDD) they are also called value objects, but it does not really matter how you call them. They encapsulate all aspects of a specific type of data and make sure that its value is valid. We can now change the DivisionRequest accordingly:

public final class DivisionRequest {
    public final Integer dividend;
    public final Divisor divisor;

    private DivisionRequest(final Integer dividend, final Divisor divisor) {
        this.dividend = dividend;
        this.divisor = divisor;
    }

    public static DivisionRequest divisionRequest(final Integer dividend,
                                                  final Divisor divisor) {
        return new DivisionRequest(dividend, divisor);
    }
}

Also the DivisionUseCase needs a small change:

public final class DivisionUseCase {

    public CalculationResponse divide(final DivisionRequest divisionRequest) {
        final int divisor = divisionRequest.divisor.value();
        final int result = divisionRequest.dividend / divisor;
        return calculationResult(result);
    }
}

When you once again request the division by zero like this:

$ curl --request POST --header 'Content-Type: application/json' --data '{"dividend": "12", "divisor": "0"}' http://localhost:1337/divide

you will receive the following validation output:

{"errors":[{"path":"divisor","message":"the divisor must not be 0"}]}

This time, the path correctly points to the affected field: divisor. It can easily be used in a frontend to present the according form validation to the user.

Advanced settings

When HttpMaid handles a validation error as seen above, it will set the status code to 400 (Client Error). You can tell HttpMaid to use another status code like this:

final HttpMaid httpMaid = anHttpMaid()
        .post("/", SomeUseCase.class)
        .configured(MapMaidConfigurators.toConfigureMapMaidUsingRecipe(mapMaidBuilder ->
                mapMaidBuilder.withExceptionIndicatingValidationError(SomeValidationException.class)))
        .configured(MapMaidConfigurators.toSetStatusCodeOnMapMaidValidationErrorsTo(401))
        .build();

If you do not want HttpMaid to create detailed responses for validation errors, you can disable that feature. HttpMaid will instead throw an exception that can be handled accordingly. Example:

final HttpMaid httpMaid = anHttpMaid()
        .post("/", SomeUseCase.class)
        .configured(MapMaidConfigurators.toConfigureMapMaidUsingRecipe(mapMaidBuilder ->
                mapMaidBuilder.withExceptionIndicatingValidationError(SomeValidationException.class)))
        .configured(MapMaidConfigurators.toNotCreateAnAutomaticResponseForMapMaidValidationErrors())
        .configured(ExceptionConfigurators.toMapExceptionsOfType(AggregatedValidationException.class, (exception, response) -> {
            // handle validation errors here
        }))
        .build();