13

I have controller implemented with Spring Boot Rest:

@RestController
@RequestMapping("/example")
public class ExampleController {

    @Autowired
    private ExampleService exampleService;

    @GetMapping("/{id}")
    public ExampleResponse getExample(@NotNull @PathVariable("id") String id) {
        return exampleService.getExample(id);
    }
}

And response DTO:

public class ExampleResponse {

    @NotNull
    private String id;

    @NotNull
    private String otherStuff;

    // setters and getters
}

Response body is not validated. I have annotated it with @Valid but null values still pass. Request validation works well.

How to validate response body?

Justinas Jakavonis
  • 8,220
  • 10
  • 69
  • 114
  • can u share service logics? how u r returning in exampleService? – Balasubramanian Sep 14 '17 at 08:19
  • 4
    This is the first time I hear about "response validation", Usually you just validate the request. Are you sure there is such a thing? – John Donn Sep 14 '17 at 08:25
  • 1
    I need to check that all required values are filled. I may use "@Autowired Validator validator" or check them explicitly in the service but interested if there is other way to do it. – Justinas Jakavonis Sep 14 '17 at 08:31
  • Well, leaving apart your reasons for doing that, you can validate the response manually before returning it, for example the following link should be helpful: https://stackoverflow.com/questions/19190592/manually-call-spring-annotation-validation – John Donn Sep 14 '17 at 09:04
  • What do you want to achieve? Should the null-fields be omitted, should the controller return a different response (4xx/5xx)? If you should return a different response, use manual verification of objects, if null-fields should not be serialized, use jackson annotations to leave out null-fields – Martin Hansen Sep 14 '17 at 13:22
  • I have implemented custom class which validates controller response and returns 500 if it is invalid. – Justinas Jakavonis Sep 15 '17 at 07:08

5 Answers5

7

Use @Validated on Rest Controller and @Valid on the method for which the return object has to be validated. For Example:

RestController:

@RestController
@RequestMapping("/tasks")
@Validated
public class TaskController {

    @GetMapping("/{taskId}")
    @Valid
    public TaskDTO getTask(@PathVariable UUID taskId) {
        return convertToDto(taskService.findById(taskId));
    }  

}

DTO class:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ValidTaskDTO
public class TaskDTO {

    @FutureOrPresent
    @NotNull
    private ZonedDateTime dueDate;

    @NotBlank(message = "Title cannot be null or blank")
    private String title;

    private String description;

    @NotNull
    private RecurrenceType recurrenceType;

    @Future
    @NotNull
    private ZonedDateTime repeatUntil;

}

My return object TaskDTO has null dueDate and repeatUntil. So the error message will be as shown below:

{
  "timestamp": "2021-01-20T11:09:37.303929",
  "status": 400,
  "error": "Bad Request",
  "message": "getTask.<return value>.dueDate: must not be null, getTask.<return value>.repeatUntil: must not be null",
  "path": null
}

I hope this helps. For details on custom class level constraint, have a look at this video.

6

Implemented response validator:

@Aspect
@Component
public class ControllerResponseValidator {

    Logger logger = Logger.getLogger(ControllerResponseValidator.class);

    @Autowired
    private Validator validator;

    @AfterReturning(pointcut = "execution(* com.example.controller.*.*(..))", returning = "result")
    public void validateResponse(JoinPoint joinPoint, Object result) {
        validateResponse(result);
    }

    private void validateResponse(Object object) {

        Set<ConstraintViolation<Object>> validationResults = validator.validate(object);

        if (validationResults.size() > 0) {

            StringBuffer sb = new StringBuffer();

            for (ConstraintViolation<Object> error : validationResults) {
                sb.append(error.getPropertyPath()).append(" - ").append(error.getMessage()).append("\n");
            }

            String msg = sb.toString();
            logger.error(msg);
            throw new RestException(HttpStatus.INTERNAL_SERVER_ERROR, msg);
        }
    }
}
Justinas Jakavonis
  • 8,220
  • 10
  • 69
  • 114
2

What do you expect to happen? I see few things you should consider.

  1. If an object must really not have fields with value of null, you should verify this when the objects are saved to your repository (of which ever kind of your liking). Then if your service returns something, you know it's already valid, if it returns nothing; you can return a proper status code and message for the client (4xx/5xx). You can also map specific exceptions to a certain type of status code so you can just throw the exceptions from your code, and let them be caught and handled by the default exception handler in spring-boot

  2. If your fields can be null, but you want to omit them in serialization, you can use jackson annotations. @JsonInclude(JsonInclude.Include.NON_NULL) will only serialize non-null values in the response.

Martin Hansen
  • 2,033
  • 1
  • 18
  • 34
0

Shouldn't you annotate it as following snippet?

 public @ResponseBody ExampleResponse getExample(@NotNull @PathVariable("id") String id) {
        return exampleService.getExample(id);
    }
arshovon
  • 13,270
  • 9
  • 51
  • 69
0

You can add "@Validated @ResponseBody" annotations

public @Validated @ResponseBody getExample(@NotNull @PathVariable("id") String id) {