REST API Send multiples errors in a JSON Array at the same time with JAX-RS



I’m learning how to create an API with Java EE and JAX-RS. But I could not find any answer about my problem on internet.

I have a route “signup” :

    @POST
    @Path("/signup")
    @Produces(MediaType.APPLICATION_JSON)
    public Response register(@FormParam("email") String email, @FormParam("pseudo") String pseudo, @FormParam("password") String password,@FormParam("firstname") String firstname) {
                List<HashMap<String, String>> errorMsgs = authManager.register(email, pseudo, password, firstname, lastname);

        if (errorMsgs.isEmpty()) { // Si la liste est vide, il n'y a pas eu d'erreur
            HashMap<String, String> success = new HashMap<>();
            success.put("title", "Inscription réussie");
            success.put("message", "L'utilisateur a été ajouté en base de données.");
            return Response.ok().entity(success).build();
        } else {
            HashMap<String, List<HashMap<String, String>>> errors = new HashMap<>();
            errors.put("errors", errorMsgs);

            return Response.status(422).entity(errors).build();
        }
    }

Currently, to send all the error message I need, I’m using this:

public List<HashMap<String, String>> register(String email, String pseudo, String password, String firstname, String lastname) {
        List<HashMap<String, String>> errors = new ArrayList<>();

        errors.addAll(checkEmail(email));
        errors.addAll(checkPseudo(pseudo));
        errors.addAll(checkPassword(password));
        errors.addAll(checkFirstname(firstname));
        errors.addAll(checkLastname(lastname));

        if (errors.isEmpty()) {
            userDAO.create(new User(pseudo, email, password, firstname, lastname));
        }

        return errors;
    }

    private List<HashMap<String, String>> checkEmail(String email) {
        List<HashMap<String, String>> errors = new ArrayList<>();

        if (email != null && !email.isEmpty()) {
            if (email.matches("([^.@]+)(\.[^.@]+)*@([^.@]+\.)+([^.@]+)")) {
                Optional<User> user = userDAO.findByEmail(email);
                if(user.isPresent()) {
                    HashMap<String, String> error = new HashMap<>();
                    error.put("code", CODE_FIELD_ALREADY_EXIST);
                    error.put("field", "email");
                    error.put("title", "L'adresse email existe déjà dans la base de données.");
                    error.put("message", "Essayez de choisir une autre adresse email qui n'est pas déjà utilisé.");

                    errors.add(error);
                }
            } else {
                HashMap<String, String> error = new HashMap<>();
                error.put("code", CODE_BAD_FORMAT);
                error.put("field", "email");
                error.put("title", "L'adresse email n'est pas au bon format.");
                error.put("message", "Vous devez entrer un email au format valide (exemple@domaine.fr)");
                errors.add(error);
            }
        } else {
            HashMap<String, String> error = new HashMap<>();
            error.put("code", CODE_EMPTY_FIELD);
            error.put("field", "email");
            error.put("title", "Le champ email est vide.");
            error.put("message", "L'adresse email est obligatoire.");
            errors.add(error);
        }

        return errors;
    }

    private List<HashMap<String, String>> checkPseudo(String pseudo) {
        List<HashMap<String, String>> errors = new ArrayList<>();

        if (pseudo != null && !pseudo.isEmpty()) {
            if (pseudo.length() > 50) {
                HashMap<String, String> error = new HashMap<>();
                error.put("code", CODE_TOO_LONG_FIELD);
                error.put("field", "pseudo");
                error.put("title", "Le pseudo est trop long.");
                error.put("message", "Votre pseudo ne peut pas faire plus de 50 caractères.");

                errors.add(error);
            } else {
                if (userDAO.findByPseudo(pseudo).isPresent()) {
                    HashMap<String, String> error = new HashMap<>();
                    error.put("code", CODE_FIELD_ALREADY_EXIST);
                    error.put("field", "pseudo");
                    error.put("title", "Le pseudo " + pseudo + " existe déjà dans la base de données.");
                    error.put("message", "Essayez de choisir un pseudo qui n'est pas déjà utilisé.");

                    errors.add(error);
                }
            }
        } else {
            HashMap<String, String> error = new HashMap<>();
            error.put("code", CODE_EMPTY_FIELD);
            error.put("field", "pseudo");
            error.put("title", "Le champ pseudo est vide");
            error.put("message", "Le pseudo est obligatoire.");
            errors.add(error);
        }

        return errors;
    }

    private List<HashMap<String, String>> checkPassword(String password) {
        List<HashMap<String, String>> errors = new ArrayList<>();

        if (password != null && !password.isEmpty()) {
            if (password.length() < 8) {
                HashMap<String, String> error = new HashMap<>();
                error.put("code", CODE_TOO_SHORT_FIELD);
                error.put("field", "password");
                error.put("title", "Le mot de passe est trop court.");
                error.put("message", "Votre mot de passe doit faire au minimum 8 caractères.");

                errors.add(error);
            }
        } else {
            HashMap<String, String> error = new HashMap<>();
            error.put("code", CODE_EMPTY_FIELD);
            error.put("field", "password");
            error.put("title", "Le champ est vide");
            error.put("message", "Le mot de passe est obligatoire.");

            errors.add(error);
        }

        return errors;
    }

    private List<HashMap<String, String>> checkFirstname(String firstname) {
        List<HashMap<String, String>> errors = new ArrayList<>();

        if (firstname != null && !firstname.isEmpty()) {
            if (firstname.length() > 50) {
                HashMap<String, String> error = new HashMap<>();
                error.put("code", CODE_TOO_LONG_FIELD);
                error.put("field", "firstname");
                error.put("title", "Le prénom est trop long");
                error.put("message", "Votre prénom doit avoir un maximum de 50 caractères.");

                errors.add(error);
            }
        } else {
            HashMap<String, String> error = new HashMap<>();
            error.put("code", CODE_EMPTY_FIELD);
            error.put("field", "firstname");
            error.put("title", "Le champ est vide");
            error.put("message", "Le prénom est obligatoire.");

            errors.add(error);
        }

        return errors;
    }

    private List<HashMap<String, String>> checkLastname(String lastname) {
        List<HashMap<String, String>> errors = new ArrayList<>();

        if (lastname != null && !lastname.isEmpty()) {
            if (lastname.length() > 50) {
                HashMap<String, String> error = new HashMap<>();
                error.put("code", CODE_TOO_LONG_FIELD);
                error.put("field", "lastname");
                error.put("title", "Le nom est trop long");
                error.put("message", "Votre nom doit avoir un maximum de 50 caractères.");

                errors.add(error);
            }
        } else {
            HashMap<String, String> error = new HashMap<>();
            error.put("code", CODE_EMPTY_FIELD);
            error.put("field", "lastname");
            error.put("title", "Le champ est vide");
            error.put("message", "Le nom est obligatoire.");

            errors.add(error);
        }

        return errors;
    }
}

This code send me basically this type of Response: enter image description here

This is what I want, but the code is really ugly and will become a real mess, I don’t think it’s a good pratice either. So I looked for a better way to do on the web. I found some people using exception handling, either using the “ExceptionMapper” or the WebApplicationException, but the problem is that it can handle only one problem at one time, and so, this is not what I need, because I want to send multiples errors responses at one time. So I wonder is one of you got a solution about this, because I didn’t find anything about my question 😐 !

Thank you 🙂 !

Answer

Here are two approaches you can take with error handling, making it more understandable

1. Using javax.validation

You can modify your JAX-RS endpoint to accept a java class and let javax.validation handle the validations for you.

This is what you JAX-RS endpoint would look like:

 @POST
    @Path("/signup")
    @Produces(MediaType.APPLICATION_JSON)
    public Response register(@Valid RegisterationOptions registrationOptions) {

      ...
}

And the RegistrationOptions.java class:

public class RegistrationOptions {

@NotNull
String email; 

@NotNull
String pseudo;

@NotNull
String password;

@NotNull
String firstname;

 .....


}

There are multipl annotations available for validations. eg. @Min, @Max, @Negative, @Size, @Email, @Future.

All of them accept a message parameter where you can log a custom messsage if that validation was not successful.

Do keep in mind though, you would need to create an Excpetion Mapper to map the validation exception thrown to a valid JAX-RS Response.

As far as I know, this will throw an exception, for any of the values that does not satisfy the conditions that you have set. It will not throw an exception for every validation. But I may be wrong.

2. Using a custom class for error messages

If you want to show the user with multiple errors, it may be cleaner and easier to create a POJO for your error objects instead of creating a list of HashMaps.

an example would be:

public class RegistrationError {
  private enum Field {
    email,password,pseudo,firstname;
    
    String getFieldFullName() {
      switch (this) {
        case email:
          return "Email Address";
        case password:
          return "Password";
        case pseudo:
          return "Pseudo";
        case firstname:
          return "First Name";
      }
    }
  }
  private enum ErrorCodes {
    FIELD_ALREADY_EXIST,EMPTY_FIELD, TOO_LONG_FIELD,
  }
  
  private final String code;
  private final String field;
  private final String title;
  private final String message;
  
  private RegistrationError(String code, String field, String title, String message) {
    this.code = code;
    this.field = field;
    this.title = title;
    this.message = message;
  }
  
  public static RegistrationError generateError(ErrorCodes errorCodes, Field field){
    final String errorTitle = RegistrationError.getErrorTitle(errorCodes, field);
    final String errorMessage = RegistrationError.getErrorMessage(errorCodes, field);
    return new RegistrationError(errorCodes.name(), field.name(), errorTitle, errorMessage);
  }
  
  public static String getErrorTitle(ErrorCodes errorCodes, Field field) {
    switch (errorCodes) {
      case FIELD_ALREADY_EXIST:
        return field.getFieldFullName() + " already exists in DB";
      case EMPTY_FIELD:
        return field.getFieldFullName() + " cannot be empty";
      case TOO_LONG_FIELD:
        return field.getFieldFullName() + " is too long";
      default:
        return field.getFieldFullName() + " is invalid";
    }
  }
  
  public static String getErrorMessage(ErrorCodes errorCodes, Field field) {
    switch (errorCodes) {
      case FIELD_ALREADY_EXIST:
        return field.getFieldFullName() + " already exists in DB";
      case EMPTY_FIELD:
        return field.getFieldFullName() + " cannot be empty";
      case TOO_LONG_FIELD:
        return field.getFieldFullName() + " is too long";
      default:
        return field.getFieldFullName() + " is invalid";
    }
  }
  
  public String getCode() {
    return code;
  }
  
  public String getField() {
    return field;
  }
  
  public String getTitle() {
    return title;
  }
  
  public String getMessage() {
    return message;
  }
}

then you could use this in your method where you verify the fields as:

public List<RegistrationError> register(String email, String pseudo, String password, String firstname, String lastname) {
    List<RegistrationError> errors = new ArrayList<>();
    
    errors.addAll(checkEmail(email));
    errors.addAll(checkPseudo(pseudo));
    errors.addAll(checkPassword(password));
    errors.addAll(checkFirstname(firstname));
    errors.addAll(checkLastname(lastname));
    
    if (errors.isEmpty()) {
      userDAO.create(new User(pseudo, email, password, firstname, lastname));
    }
    
    return errors;
  }
  
  private List<RegistrationError> checkEmail(String email) {
    List<RegistrationError> errors = new ArrayList<>();
    
    if (email != null && !email.isEmpty()) {
      if (email.matches("([^.@]+)(\.[^.@]+)*@([^.@]+\.)+([^.@]+)")) {
        Optional<User> user = userDAO.findByEmail(email);
        if(user.isPresent()) {
          errors.add(RegistrationError.generateError(RegistrationError.ErrorCodes.FIELD_ALREADY_EXIST, RegistrationError.Field.email));
        }
      } else {
        errors.add(RegistrationError.generateError(RegistrationError.ErrorCodes.EMPTY_FIELD, RegistrationError.Field.email));
      }
    } else {
      errors.add(RegistrationError.generateError(RegistrationError.ErrorCodes.EMPTY_FIELD, RegistrationError.Field.email));
    }
    
    return errors;
  }
  
  private List<RegistrationError> checkPseudo(String pseudo) {
    List<RegistrationError> errors = new ArrayList<>();
    
    if (pseudo != null && !pseudo.isEmpty()) {
      if (pseudo.length() > 50) {
        errors.add(RegistrationError.generateError(RegistrationError.ErrorCodes.EMPTY_FIELD, RegistrationError.Field.pseudo));
      } else {
        if (userDAO.findByPseudo(pseudo).isPresent()) {
          errors.add(RegistrationError.generateError(RegistrationError.ErrorCodes.FIELD_ALREADY_EXIST, RegistrationError.Field.email));
        }
      }
    } else {
      errors.add(RegistrationError.generateError(RegistrationError.ErrorCodes.EMPTY_FIELD, RegistrationError.Field.email));
    }
    
    return errors;
  }
  
  private List<RegistrationError> checkPassword(String password) {
    List<RegistrationError> errors = new ArrayList<>();
    
    if (password != null && !password.isEmpty()) {
      if (password.length() < 8) {
        errors.add(RegistrationError.generateError(RegistrationError.ErrorCodes.EMPTY_FIELD, RegistrationError.Field.email));
      }
    } else {
      errors.add(RegistrationError.generateError(RegistrationError.ErrorCodes.EMPTY_FIELD, RegistrationError.Field.email));
    }
    
    return errors;
  }
  
  private List<RegistrationError> checkFirstname(String firstname) {
    List<RegistrationError> errors = new ArrayList<>();
    
    if (firstname != null && !firstname.isEmpty()) {
      if (firstname.length() > 50) {
        errors.add(RegistrationError.generateError(RegistrationError.ErrorCodes.EMPTY_FIELD, RegistrationError.Field.email));
      }
    } else {
      errors.add(RegistrationError.generateError(RegistrationError.ErrorCodes.EMPTY_FIELD, RegistrationError.Field.email));
    }
    
    return errors;
  }
  
  private List<RegistrationError> checkLastname(String lastname) {
    List<RegistrationError> errors = new ArrayList<>();
    
    if (lastname != null && !lastname.isEmpty()) {
      if (lastname.length() > 50) {
        errors.add(RegistrationError.generateError(RegistrationError.ErrorCodes.EMPTY_FIELD, RegistrationError.Field.email));
      }
    } else {
      errors.add(RegistrationError.generateError(RegistrationError.ErrorCodes.EMPTY_FIELD, RegistrationError.Field.email));
    }
    
    return errors;
  }

(you need to correct the errors and fields before using this)

And your JAX-RS method can send this array as:

List<RegistrationError> errorMsgs = authManager.register(email, pseudo, password, firstname, lastname);

...

            HashMap<String, List<RegistrationErrors>> errors = new HashMap<>();
            errors.put("errors", errorMsgs);

            return Response.status(422).entity(errors).build();

You can also combine this approach with the javax.validation package approach if you build an Exception Mapper.



Source: stackoverflow