Validating constraints with microservices

duration 20 minutes

Prerequisites:

Explore how to use bean validation to validate user input data for microservices.

What you’ll learn

You will learn the basics of writing and testing a microservice that uses bean validation and the new functionality of Bean Validation 2.0. The service uses bean validation to validate that the supplied JavaBeans meet the defined constraints.

Bean Validation is a Java specification that simplifies data validation and error checking. Bean validation uses a standard way to validate data stored in JavaBeans. Validation can be performed manually or with integration with other specifications and frameworks, such as Contexts and Dependency Injection (CDI), Java Persistence API (JPA), or JavaServer Faces (JSF). To set rules on data, apply constraints by using annotations or XML configuration files. Bean validation provides both built-in constraints and the ability to create custom constraints. Bean validation allows for validation of both JavaBean fields and methods. For method-level validation, both the input parameters and return value can be validated.

Several additional built-in constraints are included in Bean Validation 2.0, which reduces the need for custom validation in common validation scenarios. Some of the new built-in constraints include @Email, @NotBlank, @Positive, and @Negative. Also in Bean Validation 2.0, you can now specify constraints on type parameters.

The example microservice uses both field-level and method-level validation as well as several of the built-in constraints and a custom constraint.

Getting started

The fastest way to work through this guide is to clone the Git repository and use the projects that are provided inside:

git clone https://github.com/openliberty/guide-bean-validation.git
cd guide-bean-validation

The start directory contains the starting project that you will build upon.

The finish directory contains the finished project that you will build.

Before you begin, make sure you have all the necessary prerequisites.

Try what you’ll build

The finish directory in the root of this guide contains the finished application. Give it a try before you proceed.

To try out the application, first go to the finish directory and run the following Maven goal to build the application and deploy it to Open Liberty:

cd finish
mvn liberty:run

After you see the following message, your Liberty instance is ready:

The defaultServer server is ready to run a smarter planet.

Go to the http://localhost:9080/openapi/ui URL. You see the OpenAPI user interface documenting the REST endpoints used in this guide. If you are interested in learning more about OpenAPI, read Documenting RESTful APIs. Expand the /beanvalidation/validatespacecraft POST request to validate your spacecraft bean section and click Try it out. Copy the following example input into the text box:

{
  "astronaut": {
    "name": "Libby",
    "age": 25,
    "emailAddress": "[email protected]"
  },
  "destinations": {
    "Mars": 500
  },
  "serialNumber": "Liberty0001"
}

Click Execute and you receive the response No Constraint Violations because the values specified pass the constraints you will create in this guide. Now try copying the following value into the box:

{
  "astronaut": {
    "name": "Libby",
    "age": 12,
    "emailAddress": "[email protected]"
  },
  "destinations": {
    "Mars": 500
  },
  "serialNumber": "Liberty0001"
}

This time you receive Constraint Violation Found: must be greater than or equal to 18 as a response because the age specified was under the minimum age of 18. Try other combinations of values to get a feel for the constraints that will be defined in this guide.

After you are finished checking out the application, stop the Liberty instance by pressing CTRL+C in the command-line session where you ran Liberty. Alternatively, you can run the liberty:stop goal from the finish directory in another shell session:

mvn liberty:stop

Applying constraints on the JavaBeans

Navigate to the start directory to begin.

When you run Open Liberty in dev mode, dev mode listens for file changes and automatically recompiles and deploys your updates whenever you save a new change. Run the following goal to start Open Liberty in dev mode:

mvn liberty:dev

After you see the following message, your Liberty instance is ready in dev mode:

**************************************************************
*    Liberty is running in dev mode.

Dev mode holds your command-line session to listen for file changes. Open another command-line session to continue, or open the project in your editor.

First, create the JavaBeans to be constrained.

Create the Astronaut class.
src/main/java/io/openliberty/guides/beanvalidation/Astronaut.java

Astronaut.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2018, 2022 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12package io.openliberty.guides.beanvalidation;
13
14import java.io.Serializable;
15import jakarta.validation.constraints.Max;
16import jakarta.validation.constraints.Min;
17import jakarta.validation.constraints.NotBlank;
18import jakarta.validation.constraints.Email;
19
20// tag::Astronaut[]
21public class Astronaut implements Serializable {
22
23    private static final long serialVersionUID = 1L;
24
25    // tag::not-blank[]
26    @NotBlank
27    // end::not-blank[]
28    // tag::Name[]
29    private String name;
30    // end::Name[]
31
32    // tag::Min[]
33    @Min(18)
34    // end::Min[]
35    // tag::Max[]
36    @Max(100)
37    // end::Max[]
38    // tag::age[]
39    private Integer age;
40    // end::age[]
41
42    // tag::Email[]
43    @Email
44    // end::Email[]
45    // tag::emailAddress[]
46    private String emailAddress;
47    // end::emailAddress[]
48
49    public Astronaut() {
50    }
51
52    public String getName() {
53        return name;
54    }
55
56    public Integer getAge() {
57        return age;
58    }
59
60    public String getEmailAddress() {
61        return emailAddress;
62    }
63
64    public void setName(String name) {
65        this.name = name;
66    }
67
68    public void setAge(Integer age) {
69        this.age = age;
70    }
71
72    public void setEmailAddress(String emailAddress) {
73        this.emailAddress = emailAddress;
74    }
75}
76// end::Astronaut[]

The bean stores the attributes of an astronaut, name, age, and emailAddress, and provides getters and setters to access and set the values.

The Astronaut class has the following constraints applied:

  • The astronaut needs to have a name. Bean Validation 2.0 provides a built-in @NotBlank constraint, which ensures the value is not null and contains one character that isn’t a blank space. The annotation constrains the name field.

  • The email supplied needs to be a valid email address. Another built-in constraint in Bean Validation 2.0 is @Email, which can validate that the Astronaut bean includes a correctly formatted email address. The annotation constrains the emailAddress field.

  • The astronaut needs to be between 18 and 100 years old. Bean validation allows you to specify multiple constraints on a single field. The @Min and @Max built-in constraints applied to the age field check that the astronaut is between the ages of 18 and 100.

In this example, the annotation is on the field value itself. You can also place the annotation on the getter method, which has the same effect.

Create the Spacecraft class.
src/main/java/io/openliberty/guides/beanvalidation/Spacecraft.java

Spacecraft.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2018, 2022 IBM Corporation and others.
  4 * All rights reserved. This program and the accompanying materials
  5 * are made available under the terms of the Eclipse Public License 2.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-2.0/
  8 *
  9 * SPDX-License-Identifier: EPL-2.0
 10 *******************************************************************************/
 11// end::copyright[]
 12package io.openliberty.guides.beanvalidation;
 13
 14import java.io.Serializable;
 15import java.util.Map;
 16import java.util.HashMap;
 17
 18import jakarta.validation.constraints.NotBlank;
 19import jakarta.validation.constraints.Positive;
 20import jakarta.validation.constraints.NotNull;
 21import jakarta.validation.constraints.AssertTrue;
 22import jakarta.inject.Named;
 23import jakarta.enterprise.context.RequestScoped;
 24import jakarta.validation.Valid;
 25
 26@Named
 27@RequestScoped
 28// tag::Spacecraft[]
 29public class Spacecraft implements Serializable {
 30
 31    private static final long serialVersionUID = 1L;
 32
 33    // tag::Valid[]
 34    @Valid
 35    // end::Valid[]
 36    // tag::Astronaut[]
 37    private Astronaut astronaut;
 38    // end::Astronaut[]
 39
 40    // tag::Map[]
 41    private Map<@NotBlank String, @Positive Integer> destinations;
 42    // end::Map[]
 43
 44    // tag::serial-num[]
 45    @SerialNumber
 46    // end::serial-num[]
 47    // tag::serialNumber[]
 48    private String serialNumber;
 49    // end::serialNumber[]
 50
 51    public Spacecraft() {
 52        destinations = new HashMap<String, Integer>();
 53    }
 54
 55    public void setAstronaut(Astronaut astronaut) {
 56        this.astronaut = astronaut;
 57    }
 58
 59    public void setDestinations(Map<String, Integer> destinations) {
 60        this.destinations = destinations;
 61    }
 62
 63    public void setSerialNumber(String serialNumber) {
 64        this.serialNumber = serialNumber;
 65    }
 66
 67    public Astronaut getAstronaut() {
 68        return astronaut;
 69    }
 70
 71    public Map<String, Integer> getDestinations() {
 72        return destinations;
 73    }
 74
 75    public String getSerialNumber() {
 76        return serialNumber;
 77    }
 78
 79    // tag::AssertTrue[]
 80    @AssertTrue
 81    // end::AssertTrue[]
 82    // tag::launchSpacecraft[]
 83    // tag::launchCode[]
 84    public boolean launchSpacecraft(@NotNull String launchCode) {
 85    // end::launchCode[]
 86        // tag::OpenLiberty[]
 87        if (launchCode.equals("OpenLiberty")) {
 88            // end::OpenLiberty[]
 89            // tag::true[]
 90            return true;
 91            // end::true[]
 92        }
 93        // tag::false[]
 94        return false;
 95        // end::false[]
 96    }
 97    // end::launchSpacecraft[]
 98}
 99// end::Spacecraft[]

The Spacecraft bean contains 3 fields, astronaut, serialNumber, and destinations. The JavaBean needs to be a CDI managed bean to allow for method-level validation, which uses CDI interceptions. Because the Spacecraft bean is a CDI managed bean, a scope is necessary. A request scope is used in this example. To learn more about CDI, see Injecting dependencies into microservices.

The Spacecraft class has the following constraints applied:

  • Every destination that is specified needs a name and a positive distance. In Bean Validation 2.0, you can specify constraints on type parameters. The @NotBlank and @Positive annotations constrain the destinations map so that the destination name is not blank, and the distance is positive. The @Positive constraint ensures that numeric value fields are greater than 0.

  • A correctly formatted serial number is required. In addition to specifying the built-in constraints, you can create custom constraints to allow user-defined validation rules. The @SerialNumber annotation that constrains the serialNumber field is a custom constraint, which you will create later.

Because you already specified constraints on the Astronaut bean, the constraints do not need to be respecified in the Spacecraft bean. Instead, because of the @Valid annotation on the field, all the nested constraints on the Astronaut bean are validated.

You can also use bean validation with CDI to provide method-level validation. The launchSpacecraft() method on the Spacecraft bean accepts a launchCode parameter, and if the launchCode parameter is OpenLiberty, the method returns true that the spacecraft is launched. Otherwise, the method returns false. The launchSpacecraft() method uses both parameter and return value validation. The @NotNull constraint eliminates the need to manually check within the method that the parameter is not null. Additionally, the method has the @AssertTrue return-level constraint to enforce that the method must return the true boolean.

Creating custom constraints

To create the custom constraint for @SerialNumber, begin by creating an annotation.

Spacecraft.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2018, 2022 IBM Corporation and others.
  4 * All rights reserved. This program and the accompanying materials
  5 * are made available under the terms of the Eclipse Public License 2.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-2.0/
  8 *
  9 * SPDX-License-Identifier: EPL-2.0
 10 *******************************************************************************/
 11// end::copyright[]
 12package io.openliberty.guides.beanvalidation;
 13
 14import java.io.Serializable;
 15import java.util.Map;
 16import java.util.HashMap;
 17
 18import jakarta.validation.constraints.NotBlank;
 19import jakarta.validation.constraints.Positive;
 20import jakarta.validation.constraints.NotNull;
 21import jakarta.validation.constraints.AssertTrue;
 22import jakarta.inject.Named;
 23import jakarta.enterprise.context.RequestScoped;
 24import jakarta.validation.Valid;
 25
 26@Named
 27@RequestScoped
 28// tag::Spacecraft[]
 29public class Spacecraft implements Serializable {
 30
 31    private static final long serialVersionUID = 1L;
 32
 33    // tag::Valid[]
 34    @Valid
 35    // end::Valid[]
 36    // tag::Astronaut[]
 37    private Astronaut astronaut;
 38    // end::Astronaut[]
 39
 40    // tag::Map[]
 41    private Map<@NotBlank String, @Positive Integer> destinations;
 42    // end::Map[]
 43
 44    // tag::serial-num[]
 45    @SerialNumber
 46    // end::serial-num[]
 47    // tag::serialNumber[]
 48    private String serialNumber;
 49    // end::serialNumber[]
 50
 51    public Spacecraft() {
 52        destinations = new HashMap<String, Integer>();
 53    }
 54
 55    public void setAstronaut(Astronaut astronaut) {
 56        this.astronaut = astronaut;
 57    }
 58
 59    public void setDestinations(Map<String, Integer> destinations) {
 60        this.destinations = destinations;
 61    }
 62
 63    public void setSerialNumber(String serialNumber) {
 64        this.serialNumber = serialNumber;
 65    }
 66
 67    public Astronaut getAstronaut() {
 68        return astronaut;
 69    }
 70
 71    public Map<String, Integer> getDestinations() {
 72        return destinations;
 73    }
 74
 75    public String getSerialNumber() {
 76        return serialNumber;
 77    }
 78
 79    // tag::AssertTrue[]
 80    @AssertTrue
 81    // end::AssertTrue[]
 82    // tag::launchSpacecraft[]
 83    // tag::launchCode[]
 84    public boolean launchSpacecraft(@NotNull String launchCode) {
 85    // end::launchCode[]
 86        // tag::OpenLiberty[]
 87        if (launchCode.equals("OpenLiberty")) {
 88            // end::OpenLiberty[]
 89            // tag::true[]
 90            return true;
 91            // end::true[]
 92        }
 93        // tag::false[]
 94        return false;
 95        // end::false[]
 96    }
 97    // end::launchSpacecraft[]
 98}
 99// end::Spacecraft[]
Replace the annotation.
src/main/java/io/openliberty/guides/beanvalidation/SerialNumber.java

SerialNumber.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2018, 2022 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12package io.openliberty.guides.beanvalidation;
13
14import java.lang.annotation.Documented;
15import java.lang.annotation.Retention;
16import java.lang.annotation.Target;
17import static java.lang.annotation.ElementType.FIELD;
18import static java.lang.annotation.RetentionPolicy.RUNTIME;
19
20import jakarta.validation.Constraint;
21import jakarta.validation.Payload;
22
23// tag::Target[]
24@Target({ FIELD })
25// end::Target[]
26// tag::Retention[]
27@Retention(RUNTIME)
28// end::Retention[]
29@Documented
30// tag::Constraint[]
31@Constraint(validatedBy = { SerialNumberValidator.class })
32// end::Constraint[]
33// tag::SerialNumber[]
34public @interface SerialNumber {
35
36    // tag::message[]
37    String message() default "serial number is not valid.";
38    // end::message[]
39
40    // tag::groups[]
41    Class<?>[] groups() default {};
42    // end::groups[]
43
44    // tag::payload[]
45    Class<? extends Payload>[] payload() default {};
46    // end::payload[]
47}
48// end::SerialNumber[]

The @Target annotation indicates the element types to which you can apply the custom constraint. Because the @SerialNumber constraint is used only on a field, only the FIELD target is specified.

When you define a constraint annotation, the specification requires the RUNTIME retention policy.

The @Constraint annotation specifies the class that contains the validation logic for the custom constraint.

In the SerialNumber body, the message() method provides the message that is output when a validation constraint is violated. The groups() and payload() methods associate this constraint only with certain groups or payloads. The defaults are used in the example.

Now, create the class that provides the validation for the @SerialNumber constraint.

Replace the SerialNumberValidator class.
src/main/java/io/openliberty/guides/beanvalidation/SerialNumberValidator.java

SerialNumberValidator.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2018, 2022 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12package io.openliberty.guides.beanvalidation;
13
14import jakarta.validation.ConstraintValidator;
15import jakarta.validation.ConstraintValidatorContext;
16
17// tag::SerialNumberValidator[]
18public class SerialNumberValidator
19    implements ConstraintValidator<SerialNumber, Object> {
20
21    @Override
22    // tag::isValid[]
23    public boolean isValid(Object arg0, ConstraintValidatorContext arg1) {
24        //Serial Numbers must start with Liberty followed by four numbers
25        boolean isValid = false;
26        if (arg0 == null) {
27            return isValid;
28        }
29        String serialNumber = arg0.toString();
30        // tag::Liberty[]
31        isValid = serialNumber.length() == 11 && serialNumber.startsWith("Liberty");
32        // end::Liberty[]
33        try {
34            Integer.parseInt(serialNumber.substring(7));
35        } catch (Exception ex) {
36            isValid = false;
37        }
38        return isValid;
39    }
40    // end::isValid[]
41}
42// end::SerialNumberValidator[]

The SerialNumberValidator class has one method, isValid(), which contains the custom validation logic. In this case, the serial number must start with Liberty followed by 4 numbers, such as Liberty0001. If the supplied serial number matches the constraint, isValid() returns true. If the serial number does not match, it returns false.

Programmatically validating constraints

Next, create a service to programmatically validate the constraints on the Spacecraft and Astronaut JavaBeans.

Spacecraft.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2018, 2022 IBM Corporation and others.
  4 * All rights reserved. This program and the accompanying materials
  5 * are made available under the terms of the Eclipse Public License 2.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-2.0/
  8 *
  9 * SPDX-License-Identifier: EPL-2.0
 10 *******************************************************************************/
 11// end::copyright[]
 12package io.openliberty.guides.beanvalidation;
 13
 14import java.io.Serializable;
 15import java.util.Map;
 16import java.util.HashMap;
 17
 18import jakarta.validation.constraints.NotBlank;
 19import jakarta.validation.constraints.Positive;
 20import jakarta.validation.constraints.NotNull;
 21import jakarta.validation.constraints.AssertTrue;
 22import jakarta.inject.Named;
 23import jakarta.enterprise.context.RequestScoped;
 24import jakarta.validation.Valid;
 25
 26@Named
 27@RequestScoped
 28// tag::Spacecraft[]
 29public class Spacecraft implements Serializable {
 30
 31    private static final long serialVersionUID = 1L;
 32
 33    // tag::Valid[]
 34    @Valid
 35    // end::Valid[]
 36    // tag::Astronaut[]
 37    private Astronaut astronaut;
 38    // end::Astronaut[]
 39
 40    // tag::Map[]
 41    private Map<@NotBlank String, @Positive Integer> destinations;
 42    // end::Map[]
 43
 44    // tag::serial-num[]
 45    @SerialNumber
 46    // end::serial-num[]
 47    // tag::serialNumber[]
 48    private String serialNumber;
 49    // end::serialNumber[]
 50
 51    public Spacecraft() {
 52        destinations = new HashMap<String, Integer>();
 53    }
 54
 55    public void setAstronaut(Astronaut astronaut) {
 56        this.astronaut = astronaut;
 57    }
 58
 59    public void setDestinations(Map<String, Integer> destinations) {
 60        this.destinations = destinations;
 61    }
 62
 63    public void setSerialNumber(String serialNumber) {
 64        this.serialNumber = serialNumber;
 65    }
 66
 67    public Astronaut getAstronaut() {
 68        return astronaut;
 69    }
 70
 71    public Map<String, Integer> getDestinations() {
 72        return destinations;
 73    }
 74
 75    public String getSerialNumber() {
 76        return serialNumber;
 77    }
 78
 79    // tag::AssertTrue[]
 80    @AssertTrue
 81    // end::AssertTrue[]
 82    // tag::launchSpacecraft[]
 83    // tag::launchCode[]
 84    public boolean launchSpacecraft(@NotNull String launchCode) {
 85    // end::launchCode[]
 86        // tag::OpenLiberty[]
 87        if (launchCode.equals("OpenLiberty")) {
 88            // end::OpenLiberty[]
 89            // tag::true[]
 90            return true;
 91            // end::true[]
 92        }
 93        // tag::false[]
 94        return false;
 95        // end::false[]
 96    }
 97    // end::launchSpacecraft[]
 98}
 99// end::Spacecraft[]

Astronaut.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2018, 2022 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12package io.openliberty.guides.beanvalidation;
13
14import java.io.Serializable;
15import jakarta.validation.constraints.Max;
16import jakarta.validation.constraints.Min;
17import jakarta.validation.constraints.NotBlank;
18import jakarta.validation.constraints.Email;
19
20// tag::Astronaut[]
21public class Astronaut implements Serializable {
22
23    private static final long serialVersionUID = 1L;
24
25    // tag::not-blank[]
26    @NotBlank
27    // end::not-blank[]
28    // tag::Name[]
29    private String name;
30    // end::Name[]
31
32    // tag::Min[]
33    @Min(18)
34    // end::Min[]
35    // tag::Max[]
36    @Max(100)
37    // end::Max[]
38    // tag::age[]
39    private Integer age;
40    // end::age[]
41
42    // tag::Email[]
43    @Email
44    // end::Email[]
45    // tag::emailAddress[]
46    private String emailAddress;
47    // end::emailAddress[]
48
49    public Astronaut() {
50    }
51
52    public String getName() {
53        return name;
54    }
55
56    public Integer getAge() {
57        return age;
58    }
59
60    public String getEmailAddress() {
61        return emailAddress;
62    }
63
64    public void setName(String name) {
65        this.name = name;
66    }
67
68    public void setAge(Integer age) {
69        this.age = age;
70    }
71
72    public void setEmailAddress(String emailAddress) {
73        this.emailAddress = emailAddress;
74    }
75}
76// end::Astronaut[]
Create the BeanValidationEndpoint class.
src/main/java/io/openliberty/guides/beanvalidation/BeanValidationEndpoint.java

BeanValidationEndpoint.java

 1// tag::copyright[]
 2/*******************************************************************************
 3 * Copyright (c) 2018, 2022 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License 2.0
 6 * which accompanies this distribution, and is available at
 7 * http://www.eclipse.org/legal/epl-2.0/
 8 *
 9 * SPDX-License-Identifier: EPL-2.0
10 *******************************************************************************/
11// end::copyright[]
12package io.openliberty.guides.beanvalidation;
13
14import java.util.Set;
15
16import jakarta.inject.Inject;
17import jakarta.validation.Validator;
18import jakarta.validation.ConstraintViolation;
19import jakarta.validation.ConstraintViolationException;
20import jakarta.ws.rs.Consumes;
21import jakarta.ws.rs.POST;
22import jakarta.ws.rs.Path;
23import jakarta.ws.rs.Produces;
24import jakarta.ws.rs.core.MediaType;
25
26import org.eclipse.microprofile.openapi.annotations.Operation;
27import org.eclipse.microprofile.openapi.annotations.media.Content;
28import org.eclipse.microprofile.openapi.annotations.media.Schema;
29import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
30
31@Path("/")
32public class BeanValidationEndpoint {
33
34    @Inject
35    Validator validator;
36
37    @Inject
38    // tag::Spacecraft[]
39    Spacecraft bean;
40    // end::Spacecraft[]
41
42    @POST
43    @Path("/validatespacecraft")
44    @Produces(MediaType.TEXT_PLAIN)
45    @Consumes(MediaType.APPLICATION_JSON)
46    @Operation(summary = "POST request to validate your spacecraft bean")
47    // tag::validate-Spacecraft[]
48    public String validateSpacecraft(
49        @RequestBody(description = "Specify the values to create the "
50                + "Astronaut and Spacecraft beans.",
51            content = @Content(mediaType = "application/json",
52                schema = @Schema(implementation = Spacecraft.class)))
53        Spacecraft spacecraft) {
54
55            // tag::ConstraintViolation[]
56            Set<ConstraintViolation<Spacecraft>> violations
57            // end::ConstraintViolation[]
58            // tag::validate[]
59                = validator.validate(spacecraft);
60            // end::validate[]
61
62            if (violations.size() == 0) {
63                return "No Constraint Violations";
64            }
65
66            StringBuilder sb = new StringBuilder();
67            for (ConstraintViolation<Spacecraft> violation : violations) {
68                sb.append("Constraint Violation Found: ")
69                .append(violation.getMessage())
70                .append(System.lineSeparator());
71            }
72            return sb.toString();
73    }
74    // end::validate-Spacecraft[]
75
76    @POST
77    @Path("/launchspacecraft")
78    @Produces(MediaType.TEXT_PLAIN)
79    @Operation(summary = "POST request to specify a launch code")
80    // tag::launchSpacecraft[]
81    public String launchSpacecraft(
82        @RequestBody(description = "Enter the launch code.  Must not be "
83                + "null and must equal OpenLiberty for successful launch.",
84            content = @Content(mediaType = "text/plain"))
85        String launchCode) {
86            try {
87                bean.launchSpacecraft(launchCode);
88                return "launched";
89            } catch (ConstraintViolationException ex) {
90                return ex.getMessage();
91            }
92    }
93    // end::launchSpacecraft[]
94}

Two resources, a validator and an instance of the Spacecraft JavaBean, are injected into the class. The default validator is used and is obtained through CDI injection. However, you can also obtain the default validator with resource injection or a JNDI lookup. The Spacecraft JavaBean is injected so that the method-level constraints can be validated.

The programmatic validation takes place in the validateSpacecraft() method. To validate the data, the validate() method is called on the Spacecraft bean. Because the Spacecraft bean contains the @Valid constraint on the Astronaut bean, both JavaBeans are validated. Any constraint violations found during the call to the validate() method are returned as a set of ConstraintViolation objects.

The method level validation occurs in the launchSpacecraft() method. A call is then made to the launchSpacecraft() method on the Spacecraft bean, which throws a ConstraintViolationException exception if either of the method-level constraints is violated.

Enabling the Bean Validation feature

Finally, add the Bean Validation feature in the application by updating the Liberty server.xml configuration file.

Replace the Liberty server.xml configuration file.
src/main/liberty/config/server.xml

server.xml

 1<server description="Liberty Server for Bean Validation Guide">
 2
 3    <featureManager>
 4        <!-- tag::beanValidation[] -->
 5        <feature>beanValidation-3.0</feature>
 6        <!-- end::beanValidation[] -->
 7        <feature>cdi-4.0</feature>
 8        <feature>restfulWS-3.1</feature>
 9        <feature>jsonb-3.0</feature>
10        <feature>mpOpenAPI-3.1</feature>
11    </featureManager>
12
13    <variable name="http.port" defaultValue="9080"/>
14    <variable name="https.port" defaultValue="9443"/>
15    <variable name="app.context.root" defaultValue="Spacecraft"/>
16
17    <httpEndpoint httpPort="${http.port}" httpsPort="${https.port}"
18        id="defaultHttpEndpoint" host="*" />
19
20   <webApplication location="guide-bean-validation.war" contextRoot="${app.context.root}"/>
21</server>

You can now use the beanValidation feature to validate that the supplied JavaBeans meet the defined constraints.

Running the application

You started the Open Liberty in dev mode at the beginning of the guide, so all the changes were automatically picked up.

Go to the http://localhost:9080/openapi/ui URL. Expand the /beanvalidation/validatespacecraft POST request to validate your spacecraft bean section and click Try it out. Copy the following example input into the text box:

{
  "astronaut": {
    "name": "Libby",
    "age": 25,
    "emailAddress": "[email protected]"
  },
  "destinations": {
    "Mars": 500
  },
  "serialNumber": "Liberty0001"
}

Click Execute and you receive the response No Constraint Violations because the values specified pass previously defined constraints.

Next, modify the following values, all of which break the previously defined constraints:

Age = 10
Email = libbybot
SerialNumber = Liberty1

After you click Execute, the response contains the following constraint violations:

Constraint Violation Found: serial number is not valid.
Constraint Violation Found: must be greater than or equal to 18
Constraint Violation Found: must be a well-formed email address

To try the method-level validation, expand the /beanvalidation/launchspacecraft POST request to specify a launch code section. Enter OpenLiberty in the text box. Note that launched is returned because the launch code passes the defined constraints. Replace OpenLiberty with anything else to note that a constraint violation is returned.

Testing the constraints

Now, write automated tests to drive the previously created service.

Create BeanValidationIT class.
src/test/java/it/io/openliberty/guides/beanvalidation/BeanValidationIT.java

BeanValidationIT.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2018, 2022 IBM Corporation and others.
  4 * All rights reserved. This program and the accompanying materials
  5 * are made available under the terms of the Eclipse Public License 2.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-2.0/
  8 *
  9 * SPDX-License-Identifier: EPL-2.0
 10 *******************************************************************************/
 11// end::copyright[]
 12package it.io.openliberty.guides.beanvalidation;
 13
 14import static org.junit.jupiter.api.Assertions.assertEquals;
 15import static org.junit.jupiter.api.Assertions.assertTrue;
 16
 17import io.openliberty.guides.beanvalidation.Astronaut;
 18import io.openliberty.guides.beanvalidation.Spacecraft;
 19
 20import java.util.HashMap;
 21
 22import jakarta.json.bind.Jsonb;
 23import jakarta.json.bind.JsonbBuilder;
 24import jakarta.ws.rs.client.Client;
 25import jakarta.ws.rs.client.ClientBuilder;
 26import jakarta.ws.rs.client.Entity;
 27import jakarta.ws.rs.core.MediaType;
 28import jakarta.ws.rs.core.Response;
 29
 30import org.junit.jupiter.api.AfterEach;
 31import org.junit.jupiter.api.BeforeEach;
 32import org.junit.jupiter.api.Test;
 33
 34public class BeanValidationIT {
 35
 36    private Client client;
 37    private static String port;
 38
 39    // tag::BeforeEach[]
 40    @BeforeEach
 41    // end::BeforeEach[]
 42    // tag::setup[]
 43    public void setup() {
 44        // tag::Client[]
 45        client = ClientBuilder.newClient();
 46        // end::Client[]
 47        port = System.getProperty("http.port");
 48    }
 49    // end::setup[]
 50
 51    @AfterEach
 52    public void teardown() {
 53        client.close();
 54    }
 55
 56    @Test
 57    // tag::testNoFieldLevelConstraintViolations[]
 58    public void testNoFieldLevelConstraintViolations() throws Exception {
 59        // tag::Astronaut[]
 60        Astronaut astronaut = new Astronaut();
 61        astronaut.setAge(25);
 62        astronaut.setEmailAddress("[email protected]");
 63        astronaut.setName("Libby");
 64        // end::Astronaut[]
 65        // tag::Spacecraft[]
 66        Spacecraft spacecraft = new Spacecraft();
 67        spacecraft.setAstronaut(astronaut);
 68        spacecraft.setSerialNumber("Liberty1001");
 69        // end::Spacecraft[]
 70        HashMap<String, Integer> destinations = new HashMap<String, Integer>();
 71        destinations.put("Mars", 1500);
 72        destinations.put("Pluto", 10000);
 73        spacecraft.setDestinations(destinations);
 74
 75        Jsonb jsonb = JsonbBuilder.create();
 76        String spacecraftJSON = jsonb.toJson(spacecraft);
 77        Response response = postResponse(getURL(port, "validatespacecraft"),
 78                spacecraftJSON, false);
 79        String actualResponse = response.readEntity(String.class);
 80        String expectedResponse = "No Constraint Violations";
 81
 82        assertEquals(expectedResponse, actualResponse,
 83                "Unexpected response when validating beans.");
 84    }
 85    // end::testNoFieldLevelConstraintViolations[]
 86
 87    @Test
 88    // tag::testFieldLevelConstraintViolation[]
 89    public void testFieldLevelConstraintViolation() throws Exception {
 90        Astronaut astronaut = new Astronaut();
 91        astronaut.setAge(25);
 92        astronaut.setEmailAddress("libby");
 93        astronaut.setName("Libby");
 94
 95        Spacecraft spacecraft = new Spacecraft();
 96        spacecraft.setAstronaut(astronaut);
 97        spacecraft.setSerialNumber("Liberty123");
 98
 99        HashMap<String, Integer> destinations = new HashMap<String, Integer>();
100        destinations.put("Mars", -100);
101        spacecraft.setDestinations(destinations);
102
103        Jsonb jsonb = JsonbBuilder.create();
104        String spacecraftJSON = jsonb.toJson(spacecraft);
105        // tag::Response[]
106        Response response = postResponse(getURL(port, "validatespacecraft"),
107                spacecraftJSON, false);
108        // end::Response[]
109        String actualResponse = response.readEntity(String.class);
110        // tag::expectedDestinationResponse[]
111        String expectedDestinationResponse = "must be greater than 0";
112        // end::expectedDestinationResponse[]
113        assertTrue(actualResponse.contains(expectedDestinationResponse),
114                "Expected response to contain: " + expectedDestinationResponse);
115        // tag::expectedEmailResponse[]
116        String expectedEmailResponse = "must be a well-formed email address";
117        // end::expectedEmailResponse[]
118        assertTrue(actualResponse.contains(expectedEmailResponse),
119                "Expected response to contain: " + expectedEmailResponse);
120        // tag::expectedSerialNumberResponse[]
121        String expectedSerialNumberResponse = "serial number is not valid";
122        // end::expectedSerialNumberResponse[]
123        assertTrue(actualResponse.contains(expectedSerialNumberResponse),
124                "Expected response to contain: " + expectedSerialNumberResponse);
125    }
126    // end::testFieldLevelConstraintViolation[]
127
128    @Test
129    // tag::testNoMethodLevelConstraintViolations[]
130    public void testNoMethodLevelConstraintViolations() throws Exception {
131        // tag::OpenLiberty[]
132        String launchCode = "OpenLiberty";
133        // end::OpenLiberty[]
134        // tag::launchSpacecraft[]
135        Response response = postResponse(getURL(port, "launchspacecraft"),
136                launchCode, true);
137        // end::launchSpacecraft[]
138
139        String actualResponse = response.readEntity(String.class);
140        String expectedResponse = "launched";
141
142        assertEquals(expectedResponse, actualResponse,
143                "Unexpected response from call to launchSpacecraft");
144
145    }
146    // end::testNoMethodLevelConstraintViolations[]
147
148    // tag::testMethodLevelConstraintViolation[]
149    @Test
150    public void testMethodLevelConstraintViolation() throws Exception {
151        // tag::incorrectCode[]
152        String launchCode = "incorrectCode";
153        // end::incorrectCode[]
154        Response response = postResponse(getURL(port, "launchspacecraft"),
155                launchCode, true);
156
157        String actualResponse = response.readEntity(String.class);
158        assertTrue(
159                // tag::actualResponse[]
160                actualResponse.contains("must be true"),
161                // end::actualResponse[]
162                "Unexpected response from call to launchSpacecraft");
163    }
164    // end::testMethodLevelConstraintViolation[]
165
166    private Response postResponse(String url, String value,
167                                  boolean isMethodLevel) {
168        if (isMethodLevel) {
169                return client.target(url).request().post(Entity.text(value));
170        } else {
171                return client.target(url).request().post(Entity.entity(value,
172                MediaType.APPLICATION_JSON));
173        }
174    }
175
176    private String getURL(String port, String function) {
177        return "http://localhost:" + port + "/Spacecraft/beanvalidation/"
178                + function;
179    }
180}

The @BeforeEach annotation causes the setup() method to execute before the test cases. The setup() method retrieves the port number for the Open Liberty and creates a Client that is used throughout the tests, which are described as follows:

  • The testNoFieldLevelConstraintViolations() test case verifies that constraint violations do not occur when valid data is supplied to the Astronaut and Spacecraft bean attributes.

  • The testFieldLevelConstraintViolation() test case verifies that the appropriate constraint violations occur when data that is supplied to the Astronaut and Spacecraft attributes violates the defined constraints. Because 3 constraint violations are defined, 3 ConstraintViolation objects are returned as a set from the validate call. The 3 expected messages are "must be greater than 0" for the negative distance specified in the destination map, "must be a well-formed email address" for the incorrect email address, and the custom "serial number is not valid" message for the serial number.

  • The testNoMethodLevelConstraintViolations() test case verifies that the method-level constraints that are specified on the launchSpacecraft() method of the Spacecraft bean are validated when the method is called with no violations. In this test, the call to the launchSpacecraft() method is made with the OpenLiberty argument. A value of true is returned, which passes the specified constraints.

  • The testMethodLevelConstraintViolation() test case verifies that a ConstraintViolationException exception is thrown when one of the method-level constraints is violated. A call with an incorrect parameter, incorrectCode, is made to the launchSpacecraft() method of the Spacecraft bean. The method returns false, which violates the defined constraint, and a ConstraintViolationException exception is thrown. The exception includes the constraint violation message, which in this example is must be true.

Spacecraft.java

  1// tag::copyright[]
  2/*******************************************************************************
  3 * Copyright (c) 2018, 2022 IBM Corporation and others.
  4 * All rights reserved. This program and the accompanying materials
  5 * are made available under the terms of the Eclipse Public License 2.0
  6 * which accompanies this distribution, and is available at
  7 * http://www.eclipse.org/legal/epl-2.0/
  8 *
  9 * SPDX-License-Identifier: EPL-2.0
 10 *******************************************************************************/
 11// end::copyright[]
 12package io.openliberty.guides.beanvalidation;
 13
 14import java.io.Serializable;
 15import java.util.Map;
 16import java.util.HashMap;
 17
 18import jakarta.validation.constraints.NotBlank;
 19import jakarta.validation.constraints.Positive;
 20import jakarta.validation.constraints.NotNull;
 21import jakarta.validation.constraints.AssertTrue;
 22import jakarta.inject.Named;
 23import jakarta.enterprise.context.RequestScoped;
 24import jakarta.validation.Valid;
 25
 26@Named
 27@RequestScoped
 28// tag::Spacecraft[]
 29public class Spacecraft implements Serializable {
 30
 31    private static final long serialVersionUID = 1L;
 32
 33    // tag::Valid[]
 34    @Valid
 35    // end::Valid[]
 36    // tag::Astronaut[]
 37    private Astronaut astronaut;
 38    // end::Astronaut[]
 39
 40    // tag::Map[]
 41    private Map<@NotBlank String, @Positive Integer> destinations;
 42    // end::Map[]
 43
 44    // tag::serial-num[]
 45    @SerialNumber
 46    // end::serial-num[]
 47    // tag::serialNumber[]
 48    private String serialNumber;
 49    // end::serialNumber[]
 50
 51    public Spacecraft() {
 52        destinations = new HashMap<String, Integer>();
 53    }
 54
 55    public void setAstronaut(Astronaut astronaut) {
 56        this.astronaut = astronaut;
 57    }
 58
 59    public void setDestinations(Map<String, Integer> destinations) {
 60        this.destinations = destinations;
 61    }
 62
 63    public void setSerialNumber(String serialNumber) {
 64        this.serialNumber = serialNumber;
 65    }
 66
 67    public Astronaut getAstronaut() {
 68        return astronaut;
 69    }
 70
 71    public Map<String, Integer> getDestinations() {
 72        return destinations;
 73    }
 74
 75    public String getSerialNumber() {
 76        return serialNumber;
 77    }
 78
 79    // tag::AssertTrue[]
 80    @AssertTrue
 81    // end::AssertTrue[]
 82    // tag::launchSpacecraft[]
 83    // tag::launchCode[]
 84    public boolean launchSpacecraft(@NotNull String launchCode) {
 85    // end::launchCode[]
 86        // tag::OpenLiberty[]
 87        if (launchCode.equals("OpenLiberty")) {
 88            // end::OpenLiberty[]
 89            // tag::true[]
 90            return true;
 91            // end::true[]
 92        }
 93        // tag::false[]
 94        return false;
 95        // end::false[]
 96    }
 97    // end::launchSpacecraft[]
 98}
 99// end::Spacecraft[]

Running the tests

Because you started Open Liberty in dev mode, you can run the tests by pressing the enter/return key from the command-line session where you started dev mode.

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running it.io.openliberty.guides.beanvalidation.BeanValidationIT
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.493 sec - in
it.io.openliberty.guides.beanvalidation.BeanValidationIT

Results :

Tests run: 4, Failures: 0, Errors: 0, Skipped: 0

When you are done checking out the service, exit dev mode by pressing CTRL+C in the command-line session where you ran Liberty.

Great work! You’re done!

You developed and tested a Java microservice by using bean validation and Open Liberty.

Guide Attribution

Validating constraints with microservices by Open Liberty is licensed under CC BY-ND 4.0

Copy file contents
Copied to clipboard

Prerequisites:

Nice work! Where to next?

What did you think of this guide?

Extreme Dislike Dislike Like Extreme Like

What could make this guide better?

Raise an issue to share feedback

Create a pull request to contribute to this guide

Need help?

Ask a question on Stack Overflow

Like Open Liberty? Star our repo on GitHub.

Star