Spring Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Introduction

Message interpolation is the process used for creating error messages for Java bean validation constraints. For example, we can see the messages by providing a null value for a field annotated with the javax.validation.constraints.NotNull annotation.

In this tutorial, we'll learn how to use the default Spring message interpolation and how to create our own interpolation mechanism.

To see examples of other libraries providing constraints besides javax.validation, take a look at Hibernate Validator Specific Constraints. We can also create a custom Spring Validation annotation.

2. Default Message Interpolation

Before getting into code snippets, let's consider an example of an HTTP 400 response with a default @NotNull constraint violation message:

{
    ....
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            ....
            "defaultMessage": "must not be null",
            ....
        }
    ],
    "message": "Validation failed for object='notNullRequest'. Error count: 1",
    ....
}

Spring retrieves the constraint violation message details from message descriptors. Each constraint defines its default message descriptor using the message attribute. But, of course, we can overwrite it with a custom value.

As an example, we'll create a simple REST controller with a POST method:

@RestController
public class RestExample {

    @PostMapping("/test-not-null")
    public void testNotNull(@Valid @RequestBody NotNullRequest request) {
        // ...
    }
}

The request body will be mapped to the NotNullRequest object, which has just one String filed annotated with @NotNull:

public class NotNullRequest {

    @NotNull(message = "stringValue has to be present")
    private String stringValue;

    // getters, setters
}

Now, when we send in a POST request that fails this validation check, we will see our custom error message:

{
    ...
    "errors": [
        {
            ...
            "defaultMessage": "stringValue has to be present",
            ...
        }
    ],
    ...
}

The only value that changes is defaultMessage. But we still get a lot of information about error codes, object name, field name, etc. To limit the number of displayed values, we can implement Custom Error Message Handling for REST API.

3. Interpolation with Message Expressions

In Spring, we can use the Unified Expression Language to define our message descriptors. This allows defining error messages based on conditional logic and also enables advanced formatting options.

To understand it more clearly, let's look at a few examples.

In every constraint annotation, we can access the actual value of a field that's being validated:

@Size(
  min = 5,
  max = 14,
  message = "The author email '${validatedValue}' must be between {min} and {max} characters long"
)
private String authorEmail;

Our error message will contain both the actual value of the property and min and max parameters of the @Size annotation:

"defaultMessage": "The author email '[email protected]' must be between 5 and 14 characters long"

Notice that for accessing external variables, we use ${} syntax, but for accessing other properties from the validation annotation, we use {}.

Using the ternary operator is also possible:

@Min(
  value = 1,
  message = "There must be at least {value} test{value > 1 ? 's' : ''} in the test case"
)
private int testCount;

Spring will convert the ternary operator to a single value in the error message:

"defaultMessage": "There must be at least 2 tests in the test case"

We can also call methods on external variables:

@DecimalMin(
  value = "50",
  message = "The code coverage ${formatter.format('%1$.2f', validatedValue)} must be higher than {value}%"
)
private double codeCoverage;

Invalid input will produce an error message with the formatted value:

"defaultMessage": "The code coverage 44.44 must be higher than 50%"

As we can see from these examples, some characters such as {, }, $, and / are used in message expressions, so we need to escape them with a backslash character before using them literally: \{, \}, \$, and \\.

4. Custom Message Interpolation

In some cases, we want to implement a custom message interpolation engine. To do so, we must first implement the javax.validation.MessageInterpolation interface:

public class MyMessageInterpolator implements MessageInterpolator {
    private final MessageInterpolator defaultInterpolator;

    public MyMessageInterpolator(MessageInterpolator interpolator) {
        this.defaultInterpolator = interpolator;
    }

    @Override
    public String interpolate(String messageTemplate, Context context) {
        messageTemplate = messageTemplate.toUpperCase();
        return defaultInterpolator.interpolate(messageTemplate, context);
    }

    @Override
    public String interpolate(String messageTemplate, Context context, Locale locale) {
        messageTemplate = messageTemplate.toUpperCase();
        return defaultInterpolator.interpolate(messageTemplate, context, locale);
    }
}

In this simple implementation, we're just changing the error message to upper-case. By doing so, our error message will look like:

"defaultMessage": "THE CODE COVERAGE 44.44 MUST BE HIGHER THAN 50%"

We also need to register our interpolator in the javax.validation.Validation factory:

Validation.byDefaultProvider().configure().messageInterpolator(
  new MyMessageInterpolator(
    Validation.byDefaultProvider().configure().getDefaultMessageInterpolator())
);

5. Conclusion

In this article, we've learned how default Spring message interpolation works and how to create a custom message interpolation engine.

And, as always, all source code is available over on GitHub.

Spring bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

Leave a Reply

avatar
  Subscribe  
Notify of