Validate an Enum with springframework validation Errors - java

TL;DR : Enum deserialization errors are not caught by org.springframework.validation.Errors in a Rest Controller
For reference: we didn't find a clean solution yet as we finally decided that no one should call us wit a bad enum
I have a rest controller that uses org.springframework.validation.Errors for parameter validations:
#RequestMapping(value = "/vol1/frodo")
public ResponseEntity<Object> simpleMethodUsingPost(
HttpServletRequest httpServletRequest,
#Valid #RequestBody MySimpleObject simpleObject,
Errors errors) {
/* If an error occured, I need to log the object */
if (errors.hasErrors()) {
List<FieldError> fields = errors.getFieldErrors();
doSomething(fields , simpleObject);
}
}
My class MySimpleObject looks like this:
public class MySimpleObject {
#Valid
#NotNull(message = "anObjectField is a mandatory field")
private EmbeddedObject anObjectField = null;
#Valid
#NotNull(message = "aStringField is a mandatory field")
private String aStringField = null;
#Valid
private MySimpleEnum aSimpleEnum = null;
}
And my enum class MySimpleEnum is basically a class with two values:
public enum MySimpleEnum{
ORC("ORC"),
URUK("URUK");
private String value;
MySimpleEnum(String value) {
this.value = value;
}
#Override
public String toString() {
return String.valueOf(value);
}
}
The validation of this object (and the injection of errors in the springframework Error object) works well when it's on a String or an Object, but it will fail validation of an enum (hence an object containing a valid-annoted enum will fail too).
It fails when trying to cast the JSON String to an enum when the value is not valid:
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error:
Cannot deserialize value of type 'lotr.middleearth.model.MySimpleEnum' from String "HOBBIT"
This deserialization error is caught if I use a ResponseEntityExceptionHandler and override handleHttpMessageNotReadable, but then I don't have access to the different other parameters and can't use them.
How can I configure either a Validator, enum or springframework Error so that this exception is caught and usable in my controller body?

I just came across the same problem but didn't like the idea of giving the user an unformatted "ugly" validation error message.
First, I made the enum property not nullable on the pojo.
#NotNull(message = "Type must be NEW_APPLICATION or RENEWAL")
private RegistrationSubmissionTypeEnum type;
Then I changed the setter to basically check the input (as a string) and see if it matches one of the enums. If not, I do nothing, the property stays null and it's reported back as one of the validation error messages (using the message text used on the #NotNull annotation).
public void setType(Object typeInput) {
for (RegistrationSubmissionTypeEnum typeEnum : RegistrationSubmissionTypeEnum.values()) {
if (typeEnum.getKey().equalsIgnoreCase(typeInput.toString())) {
this.type=RegistrationSubmissionTypeEnum.valueOf(typeInput.toString());
}
}
}
That's really the key. The normal behavior we all despise generates an ugly error message, but it also does it in a way such that this error message is displayed alone. Personally, I like to send back all errors en masse.
I'm not a fan of hardcoding the enum values on the #NotNull message, but in this particular case (small number of enum values), it's preferable to the default enum serialization error message, and the behavior of a one-off isolated error message.
I considered a custom validator, but that started to feel heavy. Maybe someone can improve on this.

The problem that is occurring is that in the enum MySimpleEnum there is no constant "HOBBIT" the possibilities are "ORC" and "URUK", in the validation question can be used simply as in the example:
#NotNull(message = "Custom message")
private MySimpleEnum aSimpleEnum

I ended up doing something like that to extract the problematic field in the request :
int start = ex.getMessage().indexOf("[\"");
int end = ex.getMessage().indexOf("\"]");
String fieldName = exception.getMessage().substring(start + 2, end)
The field happens to be at the end of the message between the brackets.
I'm not really proud of that one, it's messy, but it seems to be the only way with enums.
I guess it would be better to use strings and proper Spring validation instead, since it depends too much on the implementation and may break with future updates.

Related

Spring-Boot REST API can't parse int array

I'm trying to create a simple SpringBoot REST api following hyperskill.org's "Web Quiz Engine" project.
But the Solve endpoint refuses to work.
Here's the controller:
#CrossOrigin(origins = "*")
#RestController
#RequestMapping("api")
#Validated
public class QuizController {
//snip
#PostMapping(value = "/quizzes/{id}/solve")
public Result solveQuiz(#PathVariable Long id, #RequestBody int[] answer){
return quizService.solveQuiz(id, answer);
}
}
Here's my postman :
stackoverflow disallows embedding images
POST http://localhost:8889/api/quizzes/1/solve?id=1
Body *raw
{
"answer": [2]
}
And the error:
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `[I` from Object value (token `JsonToken.START_OBJECT`); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type [I from Object value (token `JsonToken.START_OBJECT`)
I get the same error when I run the provided tests. Everything works until it try's to call Solve. What's most confusing is the type `[I`. What is going on?
Edit: Tried to follow the answer below:
#PostMapping(value = "/quizzes/{id}/solve")
public Result solveQuiz(#PathVariable Long id, #Valid #RequestBody AnswerArray answer){
return quizService.solveQuiz(id, answer.getAnswers());
}
and AnswerArray:
#Data
public class AnswerArray {
#NotNull
private List<Integer> answers;
}
Now the error is:
Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument 1 in public engine.Entities.Result engine.Controller.QuizController.solveQuiz(java.lang.Long,engine.Entities.AnswerArray): [Field error in object 'answerArray' on field 'answers': rejected value [null]; codes [NotNull.answerArray.answers,NotNull.answers,NotNull.java.util.List,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [answerArray.answers,answers]; arguments []; default message [answers]]; default message [must not be null]] ]
I also tried AnswerArray with an int[] and an Integer[] and also got null values. What am I missing?
well you are not posting an int[]
this is your body:
{
"answer": [
2
]
}
Which is actually a Object containing a list of integers
so your Java object should look as follows:
// The class can be named whatever
public class Request {
private List<Integer> answer;
// constructor
// getters and setters
}
And the function should look like:
public Result solveQuiz(#PathVariable Long id, #RequestBody Request answer){
return quizService.solveQuiz(id, answer.getAnswer());
}
The int[] does need to be inside a class, but the other answer left out a critical bit of information: that class needs a no-arg constructor that has an empty body. The default constructor works, but if you add an explicit constructor (or use Lombok's #Data), you no longer get the default. So adding a default constructor seems to be what I ultimately needed (by use of adding Lombok's
#NoArgsConstructor). Although I had tried that before, but Spring's lack of useful error info caused me to abandon it when there was another problem simultaneously.
In summary solveQuiz needed the int[] wrapped in an object. And that object needs an empty no-arg constructor (or default constructor), and Getter and Setter. (or Lombok #NoArgsConstructor)
So Here's my wrapper object:
#Data
#NoArgsConstructor
public class AnswerArray {
#NotNull
private int[] answer;
}
and my api function:
#PostMapping(value = "/quizzes/{id}/solve")
public Result solveQuiz(#PathVariable Long id, #Valid #RequestBody AnswerArray answer){
return quizService.solveQuiz(id, answer.getAnswer());
}
The problem may also be intellij not correctly compiling the changes, A "Rebuild Project" might have ultimately been the fix.

Switch field names between the received rest response and output to client

I have the following object and its value is set via a REST call as follows.
#Getter
#Setter
public class Benefit {
#JsonProperty("text")
private String headerText; // To note, I can't modify this headerText name
}
Data set from a rest call.
ResponseEntity<Benefit> response =
template.exchange(url, HttpMethod.POST, request, Benefit.class);
Benefit benefit = response.getBody();
The return value from the rest call is in following format which is why I annotated it as text.
{
"text" : "some text"
}
After this, using this response, I am passing it down as a value to the client that called me.
But when I send the information down, I don't want to name it as text.
I want to call it as description. Thus my response will be as follows:
{
"description" : "some text"
}
Queries/ Pointers
1. Is there a way to do this without me having to manually set it?
2. This headerText is in use for different REST call. In that scenario, I need to both
receive the value as text and also return as text. (Thus that has no issues).
3. Preferably any possible solutions, should not affect above point 2.
4. But is ok if it will affect. I will go with an entirely new Benefit2 Object to resolve this if there is a solution which affects point 2.
One possible way to do this is to set the value to another variable and pass that down as follows only for the particular rest call.
But finding it very cumbersome as follows.
Add a new field called description
#Getter
#Setter
public class Benefit {
#JsonProperty("text")
private String headerText;
// add a new field
private String description;
}
After the rest call, do the following:
Benefit benefit = response.getBody();
benefit.setDescription(benefit.getHeadlineText);
benefit.setHeaderText(null);
Any better ways?
To clarify on the flow:
Client calls my service
My service calls another service and got back:
{
"text" : "some text"
}
I then return the following back to the client.
{
"description" : "some text"
}
Thoughts after discussion.
Intention to use this object in both places, when calling rest and when returning response to client.
#Getter
#Setter
public class TestBenefit extends Benefit {
#Getter(AccessLevel.NONE)
#JsonProperty("text")
private String text;
private String description;
public void setText(String text) {
this.description = text;
}
}
Over time I learned that trying to use one object for multiple purposes in these scenarios is more trouble than it is worth. You should create objects that cater to your requests and responses appropriately. Use base classes if necessary. Also, I wouldn't call it Benefit2. :o) Name your classes, to some degree, for what they are used for. You could do something like...
class BenefitForOtherPurpose extends Benefit {
#JsonProperty('description')
public String getHeaderText() {
return this.headerText;
}
}
To that end, I don't think there is a way using the Jackson API to adjust the #JsonProperty value dynamically short of some reflection kung-fu that, again, is likely more trouble than it is worth. And there's nothing I know of in the Jackson API to conditionally set that outside of this complex solution:
Conditional JsonProperty using Jackson with Spring Boot

Jackson fail deserialization if field is not present

I have a pojo like
class Pojo {
private String string;
public String getString() { return string; }
#JsonSetter(nulls = Nulls.FAIL)
public void setString(String string) {
this.string = string;
}
}
I want to make jackson fail when deserializing if the string field is null or absent. (i.e. {"string":null} or {})
As you can see, I've succeeded in the first goal with the JsonSetter annotation. What I am hoping for now is something like that but for a missing property. I found a few other questions asking similar things but they were quite old and referenced features that might be implemented in the future. With the recent release of jackson 2.9, I was hoping maybe this is now possible.
#JsonProperty has a required element that can be used
to ensure existence of property value in JSON
Unfortunately, Jackson currently (2.9) only supports it for use with #JsonCreator annotated constructors or factory methods. Since #JsonSetter only works with setters, you'll have to do the null validation yourself.
For example, you'd define a constructor like
#JsonCreator
public Pojo(#JsonProperty(value = "string", required = true) String string) {
if (string == null) {
throw new IllegalArgumentException("string cannot be null");
}
this.string = string;
}
If the property is present, but set to null, Jackson would throw an InvalidDefinitionException that wraps the IllegalArgumentException thrown in the constructor.
If the property is absent, Jackson would throw a MismatchedInputException stating that a property is missing.
Both of these exceptions are subtypes of JsonMappingException, so you can easily deal with them the same way.
With this solution, you could also get rid of the setter altogether and make the field final if that suited your design better.
You may perform bean validation here by annotating the field of interest with #NotNull.
You may remove the annotation from your setter.
class Pojo {
#NotNull
private String string;
public String getString() { return string; }
public void setString(String string) {
this.string = string;
}
}
Similarly if you want to fail the validation for other constraints like size, pattern etc, you may use similar equivalent annotations available here.

JSR-303 / Spring MVC - validate conditionally using groups

I worked out a concept to conditionally validate using JSR 303 groups. "Conditionally" means that I have some fields which are only relevant if another field has a specific value.
Example: There is an option to select whether to register as a person or as a company. When selecting company, the user has to fill a field containing the name of the company.
Now I thought I use groups for that:
class RegisterForm
{
public interface BasicCheck {}
public interface UserCheck {}
public interface CompanyCheck {}
#NotNull(groups = BasicCheck.class)
private Boolean isCompany
#NotNull(groups = UserCheck.class)
private String firstName;
#NotNull(groups = UserCheck.class)
private String lastName;
#NotNull(groups = CompanyCheck.class)
private String companyName;
// getters / setters ...
}
In my controller, I validate step by step depending on the respective selection:
#Autowired
SmartValidator validator;
public void onRequest(#ModelAttribute("registerForm") RegisterForm registerForm, BindingResult result)
{
validator.validate(registerForm, result, RegisterForm.BasicCheck.class);
if (result.hasErrors()
return;
// basic check successful => we can process fields which are covered by this check
if (registerForm.getIsCompany())
{
validator.validate(registerForm, result, RegisterForm.CompanyCheck.class)
}
else
{
validator.validate(registerForm, result, RegisterForm.UserCheck.class);
}
if (!result.hasErrors())
{
// process registration
}
}
I only want to validate what must be validated. If the user selects "company" fills a field with invalid content and then switches back to "user", the invalid company related content must be ignored by the validator. A solution would be to clear those fields using Javascript, but I also want my forms to work with javascript disabled. This is why I totally like the approach shown above.
But Spring breaks this idea due to data binding. Before validation starts, Spring binds the data to registerForm. It adds error to result if, for instance, types are incompatible (expected int-value, but user filled the form with letters). This is a problem as these errors are shown in the JSP-view by <form:errors /> tags
Now I found a way to prevent Spring from adding those errors to the binding result by implementing a custom BindingErrorProcessor. If a field contains null I know that there was a validation error. In my concept null is not allowed - every field gets annotated with #NotNull plus the respective validation group.
As I am new to Spring and JSR-303 I wonder, whether I am totally on the wrong path. The fact that I have to implement a couple of things on my own makes me uncertain. Is this a clean solution? Is there a better solution for the same problem, as I think this is a common problem?
EDIT
Please see my answer here if you are interested in my solution in detail: https://stackoverflow.com/a/30500985/395879
You are correct that Spring MVC is a bit picky in this regard,and it is a common problem. But there are work-arounds:
Make all your backing fields strings, and do number/date etc conversions and null checks manually.
Use JavaScript to set fields to null when they become irrelevant.
Use JavaScript to validate fields when they are entered. This will fix almost all of your problems.
Good luck!
I know this question is old, but I came upon it looking for an answer for a different situation.
I think for your situation you could use inheritance for the forms and then use two controller methods:
The forms would look like this:
public class RegistrationForm
{
// Common fields go here.
}
public class UserRegistrationForm
extends RegistrationForm
{
#NotNull
private String firstName;
#NotNull
private String lastName;
// getters / setters ...
}
public class CompanyRegistrationForm
extends RegistrationForm
{
#NotNull
private String companyName;
// getters / setters ...
}
The controller methods would look like this:
#RequestMapping(method = RequestMethod.POST, params = "isCompany=false")
public void onRequest(
#ModelAttribute("registerForm") #Valid UserRegistrationForm form,
BindingResult result)
{
if (!result.hasErrors())
{
// process registration
}
}
#RequestMapping(method = RequestMethod.POST, params = "isCompany=true")
public void onRequest(
#ModelAttribute("registerForm") #Valid CompanyRegistrationForm form,
BindingResult result)
{
if (!result.hasErrors())
{
// process registration
}
}
Notice that the #RequestMapping annotations include a params attribute so the value of the isCompany parameter determines which method is called.
Also notice that the #Valid annotation is place on the form parameter.
Finally, no groups are needed in this case.

JPA - No validator could be found for type: enum

I have an entity class which uses an enum type for one of the properties, and I am getting the following exception when I try to persist the entity:
javax.validation.UnexpectedTypeException: No validator could be found for type: model.schema.BaseYesNo
Any idea why this might be happening? My thinking is that since it is an enum, it should already be validated by the compiler, so no need for some kind of validator. (code below)
The entity property:
#Enumerated( EnumType.STRING )
#Column( name = "seeded_flag" )
private BaseYesNo seededFlag;
public BaseYesNo getSeededFlag() {
return this.seededFlag;
}
public void setSeededFlag( BaseYesNo seededFlag ) {
this.seededFlag = seededFlag;
}
And the definition of the enum type:
public enum BaseYesNo {
YES( "Y" ),
NO( "N" );
private String yesNoVal;
private static Map< String, BaseYesNo > stringToEnum = new HashMap< String, BaseYesNo >();
static {
for ( BaseYesNo byn : BaseYesNo.values() ) {
BaseYesNo.stringToEnum.put( byn.toString(), byn );
}
}
BaseYesNo( String yesNoVal ) {
this.yesNoVal = yesNoVal;
}
public static BaseYesNo fromString( String dbValue ) {
return BaseYesNo.stringToEnum.get( dbValue );
}
#Override
public String toString() {
return this.yesNoVal;
}
}
I had the same error as you. The problem with mine was that I had mistakenly placed #Size on my enum property:
public class PhoneNumber {
#Size(max=30) //ERROR: this validation annotation is what caused my error
#Nonnull
#Enumerated(EnumType.STRING)
private Type type;
#Size(max = 30)
#Nonnull
private String number;
public enum Type {
Cell, Home, Work, Other
}
}
Once I removed the erroneous #Size, my error went away.
#Enumerated didn't cause any problems for me, and I doubt #Column would. Perhaps you had another validation annotation you skimmed over like I did.
For my testing, I was using hibernate-validator-4.1.0-Final.jar
I came across the same situation, but with the message No validator could be found for type int.
searching the web I found some solutions, most of them focus on changing type int to type Integer, because type Integer accept nulls.
like in here :
Validation - Empty int field
unfortunately that didn't work for me.
but when I substituted #size with #Min and #Max everything went great.
hoping this may gives you a hand.
I had the same error as you but the message was "No validator could be found for type: java.lang.Long. I had spent a lot of time reviewing the code until I saw the previous solution and thus, I removed all #Size entries from the entity code and also the problem went away. I should actually find out which is the offending line in the entity source code but coming from another more developer friendly platform (IBM i Series) I'll wait until the error messages are improved to give us better and accurate reasons.

Categories

Resources