I have created a custom date Validator for checking the timestamp string like as shown below. The code is working fine, but validation is not working and no message is showing in the response body.
When I post as data like as shown below
{
"price": "0",
"timestamp": "foo foo"
}
it is giving me 200. My exception is to get the valid exception details. Can anyone please help me on this
StockController.java
#RestController
#AllArgsConstructor
#RequestMapping(value = "/stock", produces = MediaType.APPLICATION_JSON_VALUE)
public class StockController {
#Autowired
private StockService stockService;
#PostMapping
public void createStock(#Valid #RequestBody final Stock stock) {
stockService.create(stock);
}
}
DefaultControllerAdvice.java
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
#ControllerAdvice
public class DefaultControllerAdvice {
#ExceptionHandler(Exception.class)
#ResponseBody
public Exception handleException(Exception exception){
return exception;
}
}
Stock.java
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import com.challenge.validators.TimestampValidator;
import lombok.Data;
#Data
public class Stock {
#Min(0)
#NotNull
public double price;
#NotNull(message="Timestamp cannot be empty")
#DateTimeValidator
public String timestamp;
}
DateTimeValidator.java
#Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = DateTimeValidatorCheck.class)
#Documented
public #interface DateTimeValidator {
String message() default "Must be timestamp of format YYYY-MM-DDThh:mm:ss.sssZ";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
DateTimeValidatorCheck.java
public class DateTimeValidatorCheck implements ConstraintValidator<DateTimeValidator, String> {
#Override
public void initialize(DateTimeValidator dateTimeValidator ) {
}
#Override
public boolean isValid(String timestamp, ConstraintValidatorContext context) {
if (timestamp == null) {
return false;
} else {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
try {
LocalDate.parse(timestamp, dateTimeFormatter);
return true;
} catch (DateTimeParseException dateTimeParseException) {
return false;
}
}
}
}
You need a BindingResult bindingResult parameter on your createStock method. Use this to check for validation errors:
if (bindingResult.hasErrors()) {
// return error or throw exception
}
Related
I have Java endpoint which receives json-deserializable object. Unfortunately, Swagger is unable to auto-generate good example for it. Is it possible to provide completely custom JSON for an example?
Example is below, regard class Body. It has two fields.
One field is a Set. I want to provide some example list of values for it. I can't use example parameter for this.
Another field is a Parent. It can contain one of two of subclessed, Child1 and Child2. Springfox generates me
{
"parent": {
"#child#": "string"
},
"tags": "[\"tag1\", \"tag2\"]"
}
and I can't send this value (it's incorrect serialization). While I want to have
{
"parent": {
"#child#": "1",
"field1": "value of field 1"
},
"tags": ["tag1", "tag2"]
}
The code:
package com.example.demo;
import java.io.IOException;
import java.util.Set;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.DatabindContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
#RestController
#SpringBootApplication
#Configuration
#EnableOpenApi
public class DemoApplication {
#PostMapping(value = "/create", consumes = MediaType.APPLICATION_JSON_VALUE)
public Body create(#RequestBody Body body) {
return body;
}
#Bean
public Docket docket() {
return new Docket(DocumentationType.OAS_30)
.select()
.apis(RequestHandlerSelectors.basePackage(DemoApplication.class.getPackageName()))
.paths(PathSelectors.any())
.build()
//.apiInfo(apiInfo())
//.securitySchemes(Collections.singletonList(apiKey()))
//.protocols(getProtocols(systemSettings))
;
}
public static class Body {
#ApiModelProperty(example = "[\"tag1\", \"tag2\"]")
public Set<String> tags;
public Parent parent;
}
#JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM, property = "#child#", include = JsonTypeInfo.As.EXISTING_PROPERTY, visible = true)
#JsonTypeIdResolver(MyTypeIdResolver.class)
#ApiModel(discriminator = "#child#")
public static class Parent {
final String childTypeNumber;
#JsonProperty("#child#")
public String childTypeNumber() {
return childTypeNumber;
}
public Parent(String childTypeNumber) {
this.childTypeNumber = childTypeNumber;
}
}
public static class MyTypeIdResolver extends TypeIdResolverBase {
private JavaType superType;
#Override
public void init(JavaType baseType) {
superType = baseType;
}
#Override
public String idFromValue(Object value) {
return null;
}
#Override
public String idFromValueAndType(Object value, Class<?> suggestedType) {
return null;
}
#Override
public JsonTypeInfo.Id getMechanism() {
return null;
}
#Override
public JavaType typeFromId(DatabindContext context, String id) throws IOException {
char c = id.charAt(0);
Class<?> subType = null;
switch (c) {
case '1':
subType = Child1.class;
break;
case '2':
subType = Child2.class;
break;
default:
throw new RuntimeException("Invalid Child type");
}
return context.constructSpecializedType(superType, subType);
}
}
public static class Child1 extends Parent {
public String field1;
public Child1() {
super("1");
}
}
public static class Child2 extends Parent {
public String field2;
public Child2() {
super("2");
}
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
From what I understand, you want swagger to display the resource returned by the endpoint.
If so, this is the solution:
#Operation(summary = "create new resource",
description = "create resourcey completely", responses = {
#ApiResponse(responseCode = "200",
description = "createresource",
content = {#Content(mediaType = "application/json",
schema = #Schema(implementation = Body.class))})
#PostMapping(value = "/create", consumes = MediaType.APPLICATION_JSON_VALUE)
public Body create(#RequestBody Body body) {
return body;
}
So that the controller does not have so many things left, what is done is to create the controller interface with all the annotations on the method signature, then your controller will implement the interface that already has all the documentation annotations.
I was trying to add custom annotation validation on the age field. as I saw some JSR 303 topics
Controller
#Validated
#Controller
public class StudentController {
#Autowired
private StudentRepository repository;
#PostMapping("/send")
public ResponseEntity saveStudent(#AgeValidator #RequestBody Student student) {
System.out.println("saveStudent invoked");
repository.save(student);
ResponseData responseData = new ResponseData();
responseData.setResultId("result");
responseData.setResultValue("saved");
ResponseEntity entity = new ResponseEntity(responseData, HttpStatus.OK);
return entity;
}
}
Model
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Student {
#Id
#Column(length = 7)
private String Id;
private String fullName;
private Integer age;
private String department;
}
AgeValidator
package com.example.demo4;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
#Documented
#Target({ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = AgeValidatorImpl.class)
public #interface AgeValidator {
String message()
default "Please enter a valid age";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
AgeValidatorImpl
class AgeValidatorImpl implements ConstraintValidator<AgeValidator, Student> {
#Override
public void initialize(AgeValidator constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation); //To change body of generated methods, choose Tools | Templates.
}
#Override
public boolean isValid(Student t, ConstraintValidatorContext cvc) {
System.out.println("AgeValidatorImpl invoked");
if (t.getAge() < 18) {
return false;
} else if (t.getAge() > 40) {
return false;
}
return true;
}
}
so if am sending using postman at any age it saves the record and it's not validating. I saw many peoples commented to add annotation on controller #validated which I import from import org.springframework.validation.annotation.Validated. Still Why this is not working. what am missing?
You can try this,
I have updated #Age annotation. You can provide upper and lower limit for validation. Note I that have added ElementType.FIELD to #Target. It allows you to use this in class fields as well.
#Documented
#Target({ElementType.PARAMETER, ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = AgeValidator.class)
public #interface Age {
int lower() default 14;
int upper() default 60;
String message() default "Please enter a valid age";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
This is the validation constrain for the annotation.
public class AgeValidator implements ConstraintValidator<Age, Integer> {
private int upperLimit;
private int lowerLimit;
#Override
public boolean isValid(Integer i, ConstraintValidatorContext constraintValidatorContext) {
return lowerLimit < i && i < upperLimit;
}
#Override
public void initialize(Age constraintAnnotation) {
this.lowerLimit = constraintAnnotation.lower();
this.upperLimit = constraintAnnotation.upper();
}
}
You can pass the annotation to class fields and override the upper and lower limit.
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Student {
#Id
#Column(length = 7)
private String Id;
private String fullName;
#Age(lower = 10, upper = 70)
private Integer age;
private String department;
}
Used #Validated annotation to validate the Student object against all the validation constraints.
#PostMapping("/send")
public ResponseEntity saveStudent(#Validated #RequestBody Student student)
Update
Replace this dependency,
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
<type>jar</type>
</dependency>
by this
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
The structure that I have is something like below:
Class A{
String str;
int i;
List<B> bs;
C c;
#NotNull
List<D> ds;
}
Class B{
#NotNull
List<E> es;
}
Class C{
List<String> s;
}
Class E{
#NotNull
List<String> s;
}
For the list variables that are annotated with #NotNull I need to throw validation error if any of them variables has one or more null objects. While for the other list variables I just need to remove the nulls;
What would be the best way to achieve this?
If you are using validation 2.0+ you can put annotation inside: List<#NotNull String> s;
You should define custom annotation for validating.
so define custom annotation like bellow.
#Target({ElementType.FIELD, ElementType.PARAMETER,ElementType.ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = ValidateListValidator.class)
#Documented
public #interface ValidateList {
}
and implement ValidateListValidator like this:
public class ValidateListValidator implements ConstraintValidator<ValidateList, List<Object>> {
private ValidateList validateList;
#Override
public void initialize(ValidateList validateList) {
this.validateList = validateList;
}
#Override
public boolean isValid( List<Object> list, ConstraintValidatorContext constraintValidatorContext) {
return list.stream().noneMatch(Objects::isNull);
}
}
and for test it
#Test
public void test() {
boolean valid = validator.isValid(Arrays.asList("test","this",null),context);
assertThat(valid, is(false));
}
This is the final code that I wrote, just a few tweaks to the code that Hadi posted. I hope it helps:
Annotation:
import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
#Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = ListValidator.class)
#Documented
public #interface ValidList {
String message() default "Null values are not allowed in array fields.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Validator Class:
import java.util.List;
import java.util.Objects;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class ListValidator implements ConstraintValidator<ValidList, List<? extends Object>> {
#Override
public boolean isValid(List<? extends Object> list, ConstraintValidatorContext context) {
return list.stream().noneMatch(Objects::isNull);
}
#Override
public void initialize(ValidList constraintAnnotation) {}
}
I have made custom annotation for validation of my bean field. I use #Age(value = 10) annotation for validation age. I have write code as below.
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
#Documented
#Constraint(validatedBy = AgeConstraintValidator.class)
#Target( { ElementType.METHOD, ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
public #interface Age {
String message() default "{Age is not valid }";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int value();
}
This is code for age constrain validator
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class AgeConstraintValidator implements ConstraintValidator< Age, Long> {
private Age age;
#Override
public void initialize(Age age) {
this.age = age;
}
#Override
public boolean isValid(Long dob, ConstraintValidatorContext context) {
System.out.print(" age in annotion");
if(dob != age.value()){
return true;
}
return false;
}
}
Now when i use #Age( value = 10) in my bean so it is not called Age annotation. Can anyone tell me any fault in my code. When i create my bean object and assign age differ for test but i can not get any validation on bean 's field .
Spring will not take this custom annotation automatically. You have to let Spring know about by defining a BeanPostProcessor. create a class which implements it
For e-g
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.BeansException;
public class InitHelloWorld implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("BeforeInitialization : " + beanName);
return bean; // you can return any other object as well
}
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("AfterInitialization : " + beanName);
return bean; // you can return any other object as well
}
}
and mention about this bean in your spring config xml as below
<bean class="<your pack>.InitHelloWorld" />
I want to validate two fields of a Request Class in manner that Either one field is valid OR another field is valid.
Eg:
Request Bean
public class CarRequest {
#NotEmpty
private String customerName;
#NotEmpty
private String customerId;
Controller Method
public #ResponseBody CarResponse addCar(
#ModelAttribute #Valid CarRequest request, BindingResult results)
throws RuntimeException, ValidationException {
if (results.hasErrors()) {
LOG.error("error occured while adding the car");
throw new ValidationException(
"Error Occoured while validiating car request");
}
}
Here I want to check that either customerName should be NotEmpty OR customerId should be NotEmpty. then my validation should pass. How can I implement it . Please suggest!!
You need to create custom validator to validate multiple fields.
create a custom annotation:
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
#Documented
#Constraint(validatedBy = CarRequestValidator.class)
#Target({ ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface RequestAnnotation {
String message() default "{RequestAnnotation}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
create a custom validator:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class CarRequestValidator implements
ConstraintValidator<RequestAnnotation, CarRequest> {
#Override
public void initialize(RequestAnnotation constraintAnnotation) {
}
#Override
public boolean isValid(CarRequest value, ConstraintValidatorContext context) {
// validation logic goes here
return false;
}
}
Now, annotate your model with custom annotation:
#RequestAnnotation
public class CarRequest {
private String customerName;
private String customerId;
}