19 April 2020

Summary

Validation of input data is a crucial requirement for any serious application. This also applies to REST services implemented with JAX/RS. If you follow a Design-First development approach with Swagger/OpenAPI, server stubs and model objects are usually generated. The generated code of API's model classes already contain JSR 380 annotations, which trigger validation of received request objects by the Bean Validation 2.0 framework. This article is about application-specific extension of validation rules for generated model classes.

Introduction

Implementing JAX/RS REST APIs with OpenApi following a Design-First approach starts with the formal specification of the REST API. The OpenAPI generators are used to create server stubs as well as client API code. For Java JAX/RS generated servers, the request model classes already contain validations in form of JSR 380 annotations like @NotNull. But because of limitations in validation specification with OpenAPI or gaps of the code generators, more often than not, validation rules need to be added to the generated model classes.

JSR 380

Jakarta EE 8 includes the version 2.0 of JSR 380, the Bean Validation standard for checking validity of model objects. The bean validation is mainly applied to request parameters and persisted entity objects.

Bean Validation standardizes constraint definition, declaration and validation for the Java platform. Its first two incarnations have been widely popular amongst the Java community in both, SE and EE environments. Integrations with many other specifications (CDI, JAX-RS, JPA, etc.) and frameworks have been created.

I’d like illustrate the issue and possible solutions of extending the request object validation for REST APIs by a simplified, yet realistic example. You’ll find the sample project in the folder bean-validation of the code branch of the Blog repository.

The sample application is packaged as web application (WAR file) and for tests deployed to a Wildfly 19 application server.

Example

Let’s say, you’re going to define a REST API to manage Ranges defined by a minimum and maximum integer values. The following OpenAPI snippet defines the Range object:

  components:
    schemas:
      Range:
        properties:
          min:
            type: "integer"
            minimum: 1
            maximum: 1000
          max:
            type: "integer"
            minimum: 1
            maximum: 1000

The OpenAPI generator Java JAX/RS spec creates the following model class from the specification:

public class Range   {

  private @Valid Integer min;
  private @Valid Integer max;

  /**
   * minimum: 1
   * maximum: 1000
   **/
  public Range min(Integer min) {
    this.min = min;
    return this;
  }

  @ApiModelProperty(value = "")
  @JsonProperty("min")
  @Min(1) @Max(1000)  public Integer getMin() {
    return min;
  }
  public void setMin(Integer min) {
    this.min = min;
  }

  /**
   * minimum: 1
   * maximum: 1000
   **/
  public Range max(Integer max) {
    this.max = max;
    return this;
  }

  @ApiModelProperty(value = "")
  @JsonProperty("max")
  @Min(1) @Max(1000)  public Integer getMax() {
    return max;
  }
  public void setMax(Integer max) {
    this.max = max;
  }

  @Override
  public boolean equals(java.lang.Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    Range range = (Range) o;
    return Objects.equals(this.min, range.min) &&
        Objects.equals(this.max, range.max);
  }

  @Override
  public int hashCode() {
    return Objects.hash(min, max);
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append("class Range {\n");

    sb.append("    min: ").append(toIndentedString(min)).append("\n");
    sb.append("    max: ").append(toIndentedString(max)).append("\n");
    sb.append("}");
    return sb.toString();
  }

  /**
   * Convert the given object to string with each line indented by 4 spaces
   * (except the first line).
   */
  private String toIndentedString(java.lang.Object o) {
    if (o == null) {
      return "null";
    }
    return o.toString().replace("\n", "\n    ");
  }
}

As you can see, the generated model class already contains Bean Validation annotations like @Min and @Max. Because JAX/RS integrates with Bean Validation this annotations are considered when the also generated API classes are called. Such a API class may be a POST to create a new Range resource:

  paths:
    /range:
      post:
        operationId: newRange
        requestBody:
          required: true
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Range'
        responses:
          201:
            description: "Range resource created successfully"

Without any manually written code the deployed service responds with HTTP status 400 (Bad Request) when called with a Range object, that violates one of the bean validation rules. The following curl statement illustrates the generated API:

$ curl localhost:8080/bean-validation/range -d '{ "min": 0, "max": 1 }' \
      -H "Content-Type: application/json" -H "Accept: application/json" -o - \
 | jq
...
{
  "exception": null,
  "fieldViolations": [],
  "propertyViolations": [],
  "classViolations": [],
  "parameterViolations": [
    {
      "constraintType": "PARAMETER",
      "path": "newRange.arg0.min",
      "message": "must be greater than or equal to 1",
      "value": "0"
    }
  ],
  "returnValueViolations": []
}

Up to this point everything seems to be fine. But we miss an important validation rule: The provided minimum value has to be equal or smaller than the maximum contained in the request. Unfortunately, such a rule can neither expressed by an OpenAPI specification nor considered by any generator available. This basically applies to all validations taking multiple fields of a class into account. But how do we implement such validations?

Requirements

Before we discuss different implementation options, let’s list the requirements of request validation from a API user’s point of view:

  1. Validation rule violations should be reported the same way as JSR 380 detected violations, ie. no mix of JSR 380 and application formatted violation reports.

  2. The violation report returned by the application server should consider the Accept header given by the client, ie. as shown in example above, if the request contains a header Accept: application/json, the response should be in JSON format.

  3. Invalid requests should create a response containing all violated rules. It’s not reasonable for a user if some violations are reported, fixed by the user and on the next request some other violations get reported - such behavior is rather annoying.

The listed requirements are far from being complete and focus more on the reporting of validation rule violation, the area relating to REST API and affecting the API’s user mostly.

With the requirements in mind, some implementation ideas appear immediately as insufficient:

  • Changing generated code is generally not an option, because after re-generation the manual changes are overwritten and get lost.

  • Because of the individual nature of the validation rules, adapting the code generator, eg. with custom templates, don’t look promising.

  • Moving validation checks into the business code would ignore most of the requirements listed above.

CDI based Solution

One design pattern I’ve seen in professional projects is based on the fact, that Bean Validation is not only integrated with JAX/RS, but also with CDI. For the range sample the implementation of this pattern requires the definition of a wrapper class around the Range object, that implements the additional validation as JSR 380 rule:

public class ValidateableRange {

	private final Range range;

	public ValidateableRange(Range range) {
		this.range = range;
	}

	@Valid
	public Range getRange() {
		return range;
	}

	@AssertTrue(message = "min must be less than or equal to max")
	public boolean isMinLessThanOrEqualToMax() {
		return range.getMin() <= range.getMax();
	}
}

In addition, a validator CDI bean is required:

@ApplicationScoped
public class ApplicationValidator {

	public void validate(@Valid ValidateableRange validateableRange) {
		// no-op
	}
}

Please note, that the validator can contain several validation methods for different objects, that need to be validated. Please also note, that the validation method is empty. The @Valid annotation attached to the method argument is the crucial piece, that triggers the Bean Validation when the method is called.

Given these classes, the validation can be triggered before the actual processing of the request is started:

@Inject ApplicationValidator appValidator;

public Response newRange(Range range) {
  appValidator.validate(new ValidateableRange(range));

  // business logic goes here
}

The implementation of this pattern satisfies all requirements, but the 3rd one: Request validation is performed two times, first when the request enters the JAX/RS API method and second when the ApplicationValidator enters the stage. Because the ApplicationValidator performs additional checks, invalid requests fixed by the user may report more violations when called the second time.

There are more problems with this pattern:

  • Some validation may be checked twice. That can become problematic when checking the validation rules is rather expensive in terms of resource consumption/performance.

  • The reported objects may be of type ValidateableRange, although, the API user requested a Range object.

However, for many projects, in particular for internal APIs, the behavior implemented by this CDI based validation pattern is sufficient. In addition, it’s entirely based on Java code and therefor very flexible.

But if you offer a public REST API, the requirements of API consistency and quality may be higher, also in terms of request validation.

General JAX/RS Solution

To satisfy all requirements listed above, the solution need to be based on JAX/RS and Bean Validation solely. The question is, how should this be done when the JAX/RS resources are generated and not easy modifiable?

To begin, let’s ignore the fact, that some code is generated. A class' validation rule like the sample above min must be less or equal to max, can be implemented:

  • Generically with a @AssertTrue (or @AssertFalse) annotated method like in ValidatebleRange example above.

  • More specific with a custom validator, that can be associated by a custom annotation to the model class to be validated.

Because the generic solution would require the modification of the generated code, let’s have a look at the second option. Therefor a validator as well as a annotation like the following are needed in our range sample:

public class RangeValidator implements ConstraintValidator<CheckMinLessOrEqualMax, Range> {

	public void initialize(CheckMinLessOrEqualMax arg0) {
	}

	public boolean isValid(Range range, ConstraintValidatorContext context) {
		return range.getMin() <= range.getMax();
	}
}
@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = RangeValidator.class)
@Documented
public @interface CheckMinLessOrEqualMax {
	String message() default "{net.gunther.jee.beansvalidation.xml.RangeValidator." + "message}";

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};
}

The defined RangeValidator is typically attached to the objects of range Range by annotating this class with @CheckMinLessOrEqualMax. Because the Range class is a generated API model class, this is not possible. However, like most Jakarta EE APIs, beside annotations, XML deployment descriptor are supported for configuration of the API’s objects or beans. For our example we just create a META-INF/validation.xml deployment descriptor to activate custom Bean Validation configurations:

<validation-config
	xmlns="http://xmlns.jcp.org/xml/ns/validation/configuration"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/validation/configuration
            http://xmlns.jcp.org/xml/ns/validation/configuration/validation-configuration-2.0.xsd"
	version="2.0">

	<default-provider>org.hibernate.validator.HibernateValidator</default-provider>
	<message-interpolator>org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator</message-interpolator>
	<constraint-mapping>META-INF/validation/constraints-range.xml</constraint-mapping>
</validation-config>

This deployment descriptor references a META-INF/validation/constraints-range.xml file, that eventually attaches the custom annotation CheckMinLessOrEqualMax to the Range model class:

<?xml version="1.0" encoding="UTF-8"?>
<constraint-mappings
	xmlns="http://xmlns.jcp.org/xml/ns/validation/mapping"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/validation/mapping
            http://xmlns.jcp.org/xml/ns/validation/mapping/validation-mapping-2.0.xsd"
	version="2.0">

	<default-package>net.gunther.blog.codegen.models</default-package>

	<bean class="Range" ignore-annotations="false">
		<class>
			<constraint annotation="net.gunther.jee.beansvalidation.xml.CheckMinLessOrEqualMax">
				<message>min must be less than or equal to max</message>
			</constraint>
		</class>
	</bean>

</constraint-mappings>

With that in place, the request validation satisfies all requirements listed above:

$ curl localhost:8080/bean-validation/range -d '{ "min": 1001, "max": 0 }' \
       -H "Content-Type: application/json" -H "Accept: application/json" -o - -v \
     | jq
...
> POST /bean-validation/range HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.65.3
> Content-Type: application/json
> Accept: application/json
...
>
...
< HTTP/1.1 400 Bad Request
< validation-exception: true
< Content-Type: application/json
...

{
  "exception": null,
  "fieldViolations": [],
  "propertyViolations": [],
  "classViolations": [],
  "parameterViolations": [
    {
      "constraintType": "PARAMETER",
      "path": "newRange.arg0.max",
      "message": "must be greater than or equal to 1",
      "value": "0"
    },
    {
      "constraintType": "PARAMETER",
      "path": "newRange.arg0.min",
      "message": "must be less than or equal to 1000",
      "value": "1001"
    },
    {
      "constraintType": "PARAMETER",
      "path": "newRange.arg0",
      "message": "min must be less than or equal to max",
      "value": "class Range {\n    min: 1001\n    max: 0\n}"
    }
  ],
  "returnValueViolations": []
}

The validation is complete, ie. all validation rules are checked, regardless of how they are activated, by annotation or XML deployment descriptor. The validation is entirely based on JSR 380 Bean Validation. Because of the good integration of Bean Validation with JAX/RS, the requested response format is also considered, for example:

$ curl localhost:8080/bean-validation/range -d '{ "min": 1001, "max": 0 }' \
       -H "Content-Type: application/json" -H "Accept: application/xml" -o - -v \
     | xmllint --format -
...
> POST /bean-validation/range HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.65.3
> Content-Type: application/json
> Accept: application/xml
...
< HTTP/1.1 400 Bad Request
< validation-exception: true
< Content-Type: application/xml;charset=UTF-8
...

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<violationReport>
  <parameterViolations>
    <constraintType>PARAMETER</constraintType>
    <path>newRange.arg0</path>
    <message>min must be less than or equal to max</message>
    <value>class Range {
    min: 1001
    max: 0
}</value>
  </parameterViolations>
  <parameterViolations>
    <constraintType>PARAMETER</constraintType>
    <path>newRange.arg0.max</path>
    <message>must be greater than or equal to 1</message>
    <value>0</value>
  </parameterViolations>
  <parameterViolations>
    <constraintType>PARAMETER</constraintType>
    <path>newRange.arg0.min</path>
    <message>must be less than or equal to 1000</message>
    <value>1001</value>
  </parameterViolations>
</violationReport>
I’ve found XML descriptors also useful in other use cases, when the concerned objects/beans are third-party and not owned by the application project’s team. This is not only true for Bean Validation, but many other Jakarta EE standards.

Conclusion

Any serious application requires the validation of input data. That’s also true for REST APIs, which need to validate the received request objects. The team JAX/RS and Bean Validation offers a lot of possibilities, when it comes to validate the input of REST APIs. Even if the JAX/RS resources and API model classes are generated - in our example with OpenAPI generators - this article demonstrated options to apply mentioned Jakarta EE standards.

While the CDI based, Java code only solution works well and offers sufficient results for many projects, the solution applying XML deployment descriptors is best from an API’s user point of view. IMHO, there’s rarely a need to mix business code with validation logic and worsen the usability of the API.

Tags: code-generator design-first cdi openapi bean-validation jaxrs jakarta-ee swagger