Validating Web Service Requests - RESTful Web Services with Dropwizard (2014)

RESTful Web Services with Dropwizard (2014)

Chapter 7. Validating Web Service Requests

Up to this point, we have a RESTful Web Service that produces JSON representations and is also capable of storing and updating contacts. Before we actually store or update a contact's information though, we need to ensure that the provided information is valid and consistent.

Adding validation constraints

The first thing we need to do in order to validate contacts is to define what is considered a valid contact. To do so, we will modify the representation class, adding constraints to its members in the form of Hibernate Validator annotations.

How to do it…

We have the Contact class, instances of which must have a first name, a last name, and a phone number in order to be considered valid. Moreover, the length of these values must be within specific limits. Let's go through the required steps in order to apply these constraints.

Modify the Contact representation class, adding the appropriate annotations to its members (import org.hibernate.validator.constraints.* first):

1. Update the declaration of the firstName variable, adding the necessary annotations in order to indicate that this is a required property (it should not be blank), and its length should be between 2 and 255 characters.

2. @NotBlank @Length(min=2, max=255)

private final String firstName;

3. In a similar way, apply the same constraints on the lastName property.

4. @NotBlank @Length(min=2, max=255)

private final String lastName;

5. The phone field should not be longer than 30 digits, so modify the values of the relevant annotation accordingly.

6. @NotBlank @Length(min=2, max=30)

private final String phone;

How it works…

The declaration of validation constraints is annotation-based. This gives us the flexibility of directly adding the validation rules we want to the members of our representation class.

Hibernate Validator is a part of the dropwizard-core module, so we do not need to declare any additional dependencies on our pom.xml.

There's more…

The recommended way of validating objects is using the standard Bean Validation API (JSR 303). For our validation needs, we use Hibernate Validator, which is a part of the Dropwizard-core module, and the reference implementation of JSR 303. Using Hibernate Validator, we can declare field constraints such as @NotBlank and @Length, or even create and use our own custom constraints that fit our needs (you may refer to Hibernate Validator's documentation at http://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#validator-customconstraints).

List of constraint annotations

The complete list of field constraints is available on the Hibernate Validator package navigator at http://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#section-builtin-constraints.

Performing validation

We've just defined what a valid annotation is. Now, we must modify the code of our resource class in order to verify that each POST and PUT request contains a valid Contact object, based on which a contact is created or updated.

How to do it…

Let's see what needs to be modified in our resource class by performing the following steps:

1. First, we need to import some classes that will help us with the validation.

2. import java.util.Set;

3. import javax.validation.ConstraintViolation;

4. import javax.util.ArrayList;

5. import javax.validation.Validator;

import javax.ws.rs.core.Response.Status;

6. Add a final member, validator, and update the constructor method in order to initialize it.

7. private final ContactDAO contactDao; private final Validator validator;

8. public ContactResource(DBI jdbi, Validator validator) {

9. contactDao = jdbi.onDemand(ContactDAO.class); this.validator = validator;

}

10. In the App class, modify the #run() method so as to pass the environment's validator as a parameter to ContactResource during its initialization, along with jDBI.

// … // Add the resource to the environmente.jersey().register(new ContactResource(jdbi, e.getValidator()));// …

11. Update the ContactResource#createContact() method and check that the contact information is valid prior to inserting it in the database.

12. @POST

13. public Response createContact(Contact contact) throws URISyntaxException {

14. // Validate the contact's data

15. Set<ConstraintViolation<Contact>> violations = validator.validate(contact);

16. // Are there any constraint violations?

17. if (violations.size() > 0) {

18. // Validation errors occurred

19. ArrayList<String> validationMessages = new ArrayList<String>();

20. for (ConstraintViolation<Contact> violation : violations) {

21.validationMessages.add(violation.getPropertyPath().toString() +": " + violation.getMessage());

22. }

23. return Response

24. .status(Status.BAD_REQUEST)

25. .entity(validationMessages)

26. .build();

27. }

28. else {

29. // OK, no validation errors

30. // Store the new contact

31. int newContactId = contactDao.createContact(contact.getFirstName(),

32. contact.getLastName(), contact.getPhone());

33. return Response.created(new URI(String.valueOf(newContactId))).build();

34. }

}

35. Similarly, update the ContactResource#updateContact() method.

36. @PUT

37. @Path("/{id}")

38. public Response updateContact(@PathParam("id") int id, Contact contact) {

39. // Validate the updated data

40. Set<ConstraintViolation<Contact>> violations = validator.validate(contact);

41. // Are there any constraint violations?

42. if (violations.size() > 0) {

43. // Validation errors occurred

44. ArrayList<String> validationMessages = new ArrayList<String>();

45. for (ConstraintViolation<Contact> violation : violations) {

46.validationMessages.add(violation.getPropertyPath().toString() +": " + violation.getMessage());

47. }

48. return Response

49. .status(Status.BAD_REQUEST)

50. .entity(validationMessages)

51. .build();

52. }

53. else {

54. // No errors

55. // update the contact with the provided ID

56. contactDao.updateContact(id, contact.getFirstName(),

57. contact.getLastName(), contact.getPhone());

58. return Response.ok(

59. new Contact(id, contact.getFirstName(), contact.getLastName(),

60. contact.getPhone())).build();

61. }

}

62. Build and run the application from the command line in order to do some tests with the validation mechanisms we just implemented.

63. Using curl, perform an HTTP POST request to http://localhost:8080/contact/, sending contact information that is going to trigger validation errors, such as firstName and lastName with length less than 2 characters, and an empty value for the phone field in a JSON string such as the following:

64.{"firstName": "F", "lastName": "L", "phone": ""}.

#> curl -v -X POST -d '{"firstName": "F", "lastName": "L", "phone": ""}' http://localhost:8080/contact/ --header "Content-Type: application/json"

How to do it…

You will see that the response is an HTTP/1.1 400 Bad Request error, and the response payload is a JSON array containing the following error messages:

< HTTP/1.1 400 Bad Request

< Date: Tue, 28 Jan 2014 20:16:57 GMT

< Content-Type: application/json

< Transfer-Encoding: chunked

<

* Connection #0 to host localhost left intact

* Closing connection #0

["phone: length must be between 2 and 30","firstName: length must be between 2 and 255","lastName: length must be between 2 and 255","phone: may not be empty"]

How it works…

In the ContactResource#createContact() method, which is mapped to the POST requests to /contact URI, we used the environment's instance of javax.validation.Validator to validate the received contact object.

The validator's #validate() method returns a Set<ConstraintViolation<Contact>> instance, which contains the validation error that occurred, if any. We check the list's size to determine if there are any violations. If there are, we will iterate through them, extracting the validation message of each error and adding it to an ArrayList instance, which we then return as a response along with HTTP Status Code 400 – Bad Request.

Since our resource class produces a JSON output (already declared with the @Produces annotation at the class level), the ArrayList instance will be transformed to a JSON array thanks to Jackson.

There's more…

As you saw, in order to test and showcase the POST requests to the endpoint we created, we need an HTTP client. Apart from cURL, there are some really good and useful HTTP client tools available (such as Postman for Google Chrome, available athttps://chrome.google.com/webstore/detail/postman-rest-client/fdmmgilgnpjigdojojpjoooidkmcomcm) that can help us with this, and we will also create our own in the next chapter.

The @Valid annotation

Instead of using a validator object to validate the input object, we could have just annotated the contact object as @Valid on the #createContact method, as seen in the following line of code:

public Response createContact(@Valid Contact contact)

When an object is annotated with @Valid, the validation is recursively performed on it. This would have the validation triggered as soon as the method was called. In case the contact object was found invalid, then a default HTTP 422 – Unprocessable entityresponse will be generated automatically. While the validator object is more powerful and customizable, the usage of the @Valid annotation is an alternative, simple, and straightforward way to validate incoming requests. This prevents the need to return a custom, more descriptive validation error message to the caller, and sends a generic one instead.

Cross-field validation

There are cases where validation should be performed on multiple fields (properties) of an object. We can achieve this by implementing custom validation annotations that also apply class-level constraints.

Luckily enough, there's a much simpler way to achieve this. Dropwizard offers the io.dropwizard.validation.ValidationMethod annotation, which we can use in a boolean method of our representation class.

How to do it…

Here are the steps needed in order to add cross-field validation to a contact object. We will check that the contact's full name is not John Doe:

1. Add a new method in the Contact class named #isValidPerson().

2. public boolean isValidPerson() {

3. if (firstName.equals("John") && lastName.equals("Doe")) {

4. return false;

5. }

6. else {

7. return true;

8. }

}

9. Then, we need to ensure that the output of this method will never be included in the output when it is serialized by Jackson. For this, annotate the #isValidPerson() method with the @JsonIgnore annotation (com.fasterxml.jackson.annotation.JsonIgnore).

10. Finally, annotate the same method with @ValidationMethod (io.dropwizard.validation.ValidationMethod), and also provide an error message in case of validation failure.

@ValidationMethod(message="John Doe is not a valid person!")

How it works…

When the validation is triggered, the #isValidPerson() method is executed along with the custom validation code we've put there. If the method returns true, that means the constraint implied by it is satisfied. If the method returns false, that indicates a constraint violation, and the validation error message will be the one we specified along with the ValidationMethod annotation.

You can create and have as many cross-field validation methods as you want in your classes. However, note that every custom validation method must be of the return type boolean, and its name must begin with is.