Validation
Now that you’ve crafted an airtight API and documented it using Swagger, you’ll want a way of enforcing constraints so that your service is not in the habit of accepting bad requests. This is where the Limberest validation API comes in.
Let’s harken back to limberest-demo and take a look at the
MoviesService
post
method to see how validation is invoked:
public Response<JSONObject> post(Request<JSONObject> request) throws ServiceException {
validate(request);
Movie movie = getPersist().create(new Movie(request.getBody()));
return new Response<>(Status.CREATED, movie.toJson());
}
...
protected void validate(Request<JSONObject> request) throws ValidationException {
Result result = getSwaggerValidator().validate(request, true);
if (result.isError())
throw new ValidationException(result);
}
The ValidationException thrown by MoviesService.validate()
is handled specially by Limberest to trigger a service response that reflects the validation outcome.
Without worrying about how the SwaggerValidator is obtained for the moment, let’s focus on the Validator functional interface it implements:
@FunctionalInterface
public interface Validator<T> {
public Result validate(Request<T> request) throws ValidationException;
}
This stipulates a validate()
method that takes a Request and returns a Limberest validation Result.
Results are cumulative, and can be appended to each other through Result.also().
Maximum accumulation can be controlled through Result.setMaxErrors().
The parameterized type for SwaggerValidator’s Validator
implementation is JSONObject.
It evaluates the request body against the requirements spelled out in the model object annotations.
For example, look again at the
@ApiModelProperty annotation
on Movie’s year
member:
@ApiModelProperty(required=true, allowableValues="range[1900, infinity]")
private int year;
public int getYear() { return year; }
public void setYear(int year) { this.year = year; }
If you submitted a POST request to limberest-demo/movies with a value for year
of 100, you’d receive a JSON
response like this:
{"status": {
"code": 400,
"message": "year: value '100' is less than minimum (1900)"
}}
Since 100 is less than 1900, the allowableValues attribute is violated, and an error Result is returned. The response payload above comes from the built-in Jsonable class StatusResponse. It’s returned by default whenever any type of ServiceException is encountered. A payload like this implies that the indicated code and message are reflected in the HTTP protocol status code and message on the response.
Note: POST, PUT and DELETE requests in limberest-demo require authentication, so you’ll not be able to perform these operations at limberest.io. To try out these examples, you can clone limberest-demo from GitHub and deploy on your favorite servlet container or on Spring Boot. Detailed instructions are here: https://limberest.github.io/limberest/demo/.
You can replace or supplement SwaggerValidator with custom validation logic by implementing Validator yourself. The rating member on limberest-demo’s Movie class is a good illustration:
@Size(max=5)
@ApiModelProperty("Must be a multiple of 0.5")
private float rating;
public float getRating() { return rating; }
public void setRating(float rating) { this.rating = rating; }
One thing to note about this is @Size
which is not actually a Swagger annotation but rather comes from the JSR-303
Bean Validation API. The other thing that stands out is that we mention in our @ApiModelProperty annotation that
rating
must be a multiple of 0.5. But Swagger annotations do not provide a good way to enforce this requirement,
so we’ll have to take care of that ourselves. Here’s how MoviesService.getSwaggerValidator()
adds this custom logic:
protected SwaggerValidator getSwaggerValidator() {
SwaggerValidator val = new SwaggerValidator();
val.addValidator(DecimalProperty.class, (json, property, path, strict) -> {
if (property.getName().equals("rating")) {
BigDecimal value = json.getBigDecimal(property.getName());
if (value.floatValue() % 0.5 != 0)
return new Result(Status.BAD_REQUEST, path + ": value '" + value + "' must be a multiple of 0.5");
}
return new Result(Status.OK);
});
return val;
}
A POST request with a rating of 6.7 fails on two counts:
{"status": {
"code": 400,
"message": "rating: value '6.7' must be a multiple of 0.5\nrating: value '6.7' exceeds maximum (5)"
}}
By default, multiple outcomes are combined into a status with the highest individual code and a newline-separated
list of messages. This behavior can be overridden by passing a custom
Consolidator to Result.getStatus()
.
Next Topic: Access Control