In the domain model object I have the following field:
private TermStatus termStatus;
TermStatus is an enum:
public enum TermStatus {
NONE,
SUCCESS,
FAIL
}
In the DTO, I have the same field as in the domain object. The question is, how can I validate the passed value? If the API client now passes an incorrect string with the enum value as a parameter (for example, nOnE), it will not receive any information about the error, only the status 400 Bad Request. Is it possible to validate it like this, for example, in the case of javax.validation annotations like #NotBlank, #Size, where in case of an error it will at least be clear what it is. There was an idea to make a separate mapping for this, for example "items/1/complete-term" instead of direct enum transmission, so that in this case the server itself would set the SUCCESS value to the termStatus field. But as far as I know, these things don't look very good in REST API, so I need your ideas
Instead of validating enum directly, you could check whether String is valid for specific enum. To achieve such an effect you could create your own enum validation annotation.
#Documented
#Constraint(validatedBy = EnumValidatorConstraint.class)
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
#NotNull
public #interface EnumValidator {
Class<? extends Enum<?>> enum();
String message() default "must be any of enum {enum}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Then you need to implement validator to check whether String exist as a part of this enum.
public class EnumValidatorConstraint implements ConstraintValidator<EnumValidator, String> {
Set<String> values;
#Override
public void initialize(EnumValidator constraintAnnotation) {
values = Stream.of(constraintAnnotation.enumClass().getEnumConstants())
.map(Enum::name)
.collect(Collectors.toSet());
}
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return values.contains(value);
}
}
Lastly, you need to annotate your field with #EnumValidator.
#EnumValidator(enum = TermStatus.class)
private String termStatus;
In case of not matching String a MethodArgumentNotValidException will be thrown. Same as for #NotNull or other constraint validation.
Sounds like you need to implement your own response after validation and tell the API client that the data in your received DTO is invalid and return message with the actual received value (nOnE in your case) and maybe the list of your valid values (if that's not gonna be a security concern). Also, I think the ideal http status for your response would be 422 instead of a generic 400 Bad Request.
For your actual validation implementation, I think you can just directly compare the converted value from DTO to ENUM of the data you received from the API client against your ENUM values in the back-end. If equals to any of the ENUM values, then it's a valid request (200) else, 422.
Hope this helps!
You can make a utility method inside your enum like below
private String text;
TermStatus(String text) {
this.text = text;
}
public static TermStatus fromText(String text) {
return Arrays.stream(values())
.filter(bl -> bl.text.equalsIgnoreCase(text))
.findFirst()
.orElse(null);
}
And set value in dto like below
dto.setTermStatus(TermStatus.fromText(passedValue))
if(dto.getTermStatus()== null)
throw new Exception("Your message");
Hope this helps!
You should use String data type for termStatus. Because of client sends String value for this. Then you have to create Custom validation constraints to fix this as below.
ValueOfEnum
#Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
#Retention(RUNTIME)
#Documented
#Constraint(validatedBy = ValueOfEnumValidator.class)
public #interface ValueOfEnum
{
Class<? extends Enum<?>> enumClass();
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
ValueOfEnumValidator
public class ValueOfEnumValidator implements ConstraintValidator<ValueOfEnum, CharSequence>
{
private List<String> acceptedValues;
#Override
public void initialize(ValueOfEnum annotation)
{
acceptedValues = Stream.of(annotation.enumClass().getEnumConstants())
.map(Enum::name)
.collect(Collectors.toList());
}
#Override
public boolean isValid(CharSequence value, ConstraintValidatorContext context)
{
if (value == null) {
return true;
}
return acceptedValues.contains(value.toString());
}
}
Now you can #ValueOfEnum annotation for your domain model. Then add #Validated annotation in front of your controller class domain object.
#ValueOfEnum(enumClass = TermStatus.class, message = "Invalid Term Status")
private String termStatus;
Related
I'm creating a custom method level constraint for a Spring REST method but the validator is not being triggered at all. The isValid method is never called.
//
// Controller
//
#PostMapping("/{id}")
#DTOParametersMatch
public ResponseEntity<DTO> createDTO(
#PathVariable("id") #SuppressWarnings("unused") UUID id,
#Validated(CreateValidation.class) #RequestBody DTO dto
) {
System.out.println("***** createDTO called");
DTO created = dtosService.createDTO(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
//
// DTOParametersMatch
//
#Constraint(validatedBy = DTOParametersMatchValidator.class)
#Target({ METHOD, CONSTRUCTOR })
#Retention(RUNTIME)
#Documented
public #interface DTOParametersMatch {
String message() default "DTO ID in path must match DTO ID in body";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
//
// DTOParametersMatchValidator
//
#SupportedValidationTarget(PARAMETERS)
public class DTOParametersMatchValidator implements ConstraintValidator<DTOParametersMatch, Object[]> {
private static final String ILLEGAL_ARGS_BASE =
"Illegal usage of DTOParametersMatch; "
+ "requires two parameters where first is a UUID and second is a DTO.";
#Override
public boolean isValid(Object[] values, ConstraintValidatorContext context) {
System.out.println("***** DTOParametersMatchValidator isValid called");
var pathId = (UUID)v0;
var body = (DTO)v1;
return pathId.equals(body.getId());
}
}
I'm not sure if I missed something or set it up wrong..
One of my colleagues pointed out I was missing the #Validated annotation on the controller class. Once I added that, everything worked.
I created a custom annotation
#Documented
#Constraint(validatedBy = CheckGranularityValidator.class)
#Target( { ElementType.PARAMETER} )
#Retention(RetentionPolicy.RUNTIME)
public #interface CheckGranularity {
String message() default "Duration has to be a multiple of granularity";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
With a validator like so
public class CheckGranularityValidator implements ConstraintValidator<CheckGranularity, AssetCostsRequest> {
#Override
public void initialize(final CheckGranularity constraintAnnotation) {
}
#Override
public boolean isValid(final AssetCostsRequest value, final ConstraintValidatorContext context) {
return value.getRange().getDuration() % value.getGranularity() == 0;
}
}
I tried using it in my RestController
#RestController
public class CalcApiController extends CalcApi {
#Override
public ResponseEntity<String> calcProfitability(#Valid #CheckGranularity #RequestBody final AssetCostsRequest assetCostsRequest) {
return ResponseEntity.ok("Works");
}
I tried using this annotation by writing a test:
#Test
public void calcTest() {
final AssetCostsRequest request = new AssetCostsRequest()
.setRange(new TimeRange(100L, 200L))
.setGranularity(26L);
given()
.contentType(ContentType.JSON)
.body(request)
.when()
.post("/calc")
.then()
.statusCode(HttpStatus.SC_BAD_REQUEST);
}
Relevant part of AssetCostsRequest:
public class AssetCostsRequest {
#JsonProperty
#NotNull
private TimeRange range;
#JsonProperty
#NotNull
private Long granularity = 30L;
...getters & setters
}
Test method returns with 200. When I try to set a breakpoint in isValid method, it isn't hit when I run the test. I tried changing order of annotations, getting rid of #Valid, changing #Target in CheckGranularity class, nothing helped. I'm using RestAssured for testing.
How do I make it, so my annotation is properly validating a parameter?
Change CheckGranularity's target to ElementType.TYPE and add #CheckGranularity directly on AssetCostsRequest. Also remove #CheckGranularity from endpoint definition.
How it works. By adding #Valid on endpoint's parameter you tell spring to validate it. Adding validation like #CheckGranularity won't work on the same level as Valid. It has to be added somewhere inside parameters class.
I need to disable validation if the value of email is null and check on it if the value is not null.
#Email(message = "{invalidMail}")
private String email;
I found the answer and it's that almost all java validation annotations accepts null so if my value is null it's going to accept it otherwise it will check.
You can't achieve this result with the predefined set of validation annotations only.
You have to create a custom validation annotation which performs the validation based on the specifications. You can get inspired on the Baeldung's article Spring MVC Custom Validation.
Here is the annotation.
#Documented
#Constraint(validatedBy = MyEmailValidator.class) // Class which performsthe validation
#Target({ ElementType.FIELD }) // Applicable to a field
#Retention(RetentionPolicy.RUNTIME)
public #interface MyEmail {
String message() default "The email is invalid"; // The default message
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
And a class which actually performs the validation itself against the email Regex if the input is not null. Returns true otherwise as null is accepted. Note this class has to implement ConstraintValidator<A extends Annotation, T>.
public class MyEmailValidatorimplements ConstraintValidator<MyEmail, String> {
// Email Regex
private final String emailPattern= "[A-Z0-9._%+-]+#[A-Z0-9.-]+\\.[A-Z]{2,6}";
#Override
public void initialize(MyEmail myEmail) { }
#Override
public boolean isValid(String input, ConstraintValidatorContext cxt) {
if (input == null) {
return true;
} else return Pattern.matches(emailPattern, input);
}
}
I am trying to use spring to check user online input to ensure that the two characters they enter is an actual US state, is there any way of doing this, hopefully using a preset pattern? like, #State or something (if that was a legit annotation). Also, is there a good annotation commonly used for a String street, and String city field? That is other than #NotNull and #NotEmpty
Any help would be greatly appreciated!!
Unfortunately there is no out of the box however you can create your own #State annotation , all you need is to define your annotation and class implementing ConstraintValidator(which handles the validation logic) E.g.
#Constraint(validatedBy = StateConstraintValidator.class)
#Target( { ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
public #interface State {
String message() default "{State}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class StateConstraintValidator implements ConstraintValidator<String, String> {
private static final Set<String> CODE_MAP = new HashSet<>(){
{add("AR");}
{add("AK");} //add more codes ...
};
#Override
public void initialize(String state) { }
#Override
public boolean isValid(String value, ConstraintValidatorContext cxt) {
if(value == null) {
return false;
}
return CODE_MAP.contains(value);
}
}
In the similar manner you can create other annotations.
Is there a way to add variables in Dropwizard's validation error message? Something in the effect of
#ValidationMethod(message=String.format("Url cannot be null, field value = %s", fieldValue))
public boolean isNotValid() {
String fieldValue = this.getFieldValue();
return this.url == null;
}
I just want to add variable into the error message.
I found an answer. Hibernate 5.1 has error message interpolation which kinda takes care of this.
#Size(min = 0, max = 0, message="${validatedValue} is present"))
public String getErrorMessage() {
List<String> illegalValues = ImmutableList.of("illegal value");
return illegalValues;
}
It's a little hacky, but it solves the problem. Take a look at http://docs.jboss.org/hibernate/validator/5.1/reference/en-US/html/chapter-message-interpolation.html
I found a proper way to validate beans - don't use dropwizard's #ValidationMethod, but instead define your own annotation and validator:
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = MyValidator.class})
#Documented
public #interface ValidPerson {
String message() default "Bad person ${validatedValue.name}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
class MyValidator implements ConstraintValidator<ValidPerson,Person> {
#Override
public void initialize(ValidPerson annotaion) {}
#Override
public boolean isValid(Person p, ConstraintValidatorContext context) {
return false; //let's say every person is invalid :-)
}
}
I'm unfamiliar with Dropwizard, but Java's annotations are simply compile-time metadata. You can't call methods in annotation declarations simply because the Java compiler does not perform the same compile-time code execution as some other compilers, such as C or C++.