Parse RequestBody as two different objects in Spring boot - java

In my Spring Boot application (2.5.5) I get a large JSON body in the POST request to a specific endpoint. On that request I need to get both the parsed object and that whole object as a string to do some validation. The JSON object contains a lot of information that I don't need so that is not included in the Object so I can't convert it to a string.
#RestController
#RequestMapping("/example")
public class ExampleController {
#PostMapping("")
public void example(
#RequestBody String stringBody,
#RequestBody ExampleRequest exampleRequest
) {
// Validate request with 'stringBody'
// Do things with 'exampleRequest'
}
}
The best idea I had so far is to just use #RequestBody String stringBody and then convert that string to a JSON object but that is really not the ideal solution.
I know that you can't have two #RequestBody but I really need to somehow have both.

I believe that a custom HandlerMethodArgumentResolver is your best option.
For that I suggest you create a custom annotation as follows:
#Target({ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
public #interface ValidJsonSignature { }
Now you need to implement the custom HandlerMethodArgumentResolver:
public class JsonSignatureValidationArgumentResolver implements HandlerMethodArgumentResolver {
private final ObjectMapper objectMapper;
public JsonSignatureValidationArgumentResolver(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
#Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterAnnotation(ValidJsonSignature.class) != null;
}
#Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
HttpServletRequest httpServletRequest = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
String jsonPayload = StreamUtils.copyToString(httpServletRequest.getInputStream(), StandardCharsets.UTF_8);
// Do actual validation here
if (// Valid) {
// If valid, then convert String to method parameter type and return it
return objectMapper.treeToValue(objectMapper.readTree(jsonPayload), methodParameter.getParameterType());
} else {
// Throw exception if validation failed
}
}
}
Next, you need to register JsonSignatureValidationArgumentResolver as an argument resolver:
#Configuration
public class JsonSignatureValidationConfiguraion implements WebMvcConfigurer {
#Autowired
private ObjectMapper objectMapper;
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new JsonSignatureValidationArgumentResolver(objectMapper));
}
}
Last but not the least, you need to annotate the Controller attribute with #ValidJsonSignature as follows:
#RestController
#RequestMapping("/example")
public class ExampleController {
#PostMapping("")
public void example(#ValidJsonSignature ExampleRequest exampleRequest) {
}
}

Related

Using Spring #Validated Annotation to Validate a Get Request Path Variable

I'm trying to use the Spring Validator and #Validated annotation to validate a Get Request parameter but cannot get the validator to run. I'm using a ModelAttribute to try and get the validator to run on the Path Variable instead of the Request Body. Is it possible to run a validator on a Get Request Path Variable?
Here is my controller class and method
#RestController
public class ProfileController {
#RequestMapping(value = "/profile/{param}", method = RequestMethod.GET)
public IVRProfile getProfile(#Validated(ParamValidator.class) #ModelAttribute("param") String param) {
return sampleProfile();
}
#ModelAttribute("param")
public String paramAsModelAttribute(#PathVariable String param) {
return param;
}
}
And the Validator class
#Component
public class ParamValidator implements Validator
{
#Override
public boolean supports(Class<?> clazz)
{
System.out.println("Validator supports test");
return String.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors)
{
System.out.println("Validator Test");
// Validation code
}
}
Neither prints statements are executed when hitting the endpoint.
Any help on what I could be missing or do differently would be greatly appreciated, thanks!
You can implement desired validation functionality as following.
public class ParamValidator implements ConstraintValidator<ParamConstraint, String> {
#Override
public void initialize(ParamConstraint paramConstraint) {
}
#Override
public boolean isValid(String paramField, ConstraintValidatorContext cxt) {
//Perform paramField validation
return true;
}
}
-
#Documented
#Constraint(validatedBy = ParamValidator.class)
#Target( { ElementType.PARAMETER })
#Retention(RetentionPolicy.RUNTIME)
public #interface ParamConstraint {
String message() default "Default validation message";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
-
#RequestMapping(value = "/profile/{param}", method = RequestMethod.GET)
public IVRProfile getProfile(#Valid #ParamConstraint #ModelAttribute("param") String param) {
return sampleProfile();
}
And finally don't forget to annotate Controller with #Validated.
#RestController
#Validated
public class ProfileController {
//...
}
More details you can find in the example as mentioned here.
You can create the answer you want by using the fields in the ConstraintViolationException with the following method;
#ExceptionHandler(ConstraintViolationException.class)
protected ResponseEntity<Object> handlePathVariableError(final ConstraintViolationException exception) {
log.error(exception.getMessage(), exception);
final List<SisSubError> subErrors = new ArrayList<>();
exception.getConstraintViolations().forEach(constraintViolation -> subErrors.add(generateSubError(constraintViolation)));
final SisError error = generateErrorWithSubErrors(VALIDATION_ERROR, HttpStatus.BAD_REQUEST, subErrors);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
You need to added an #Validated annotation to Controller class and any validation annotation before path variable field
If you want to get single RequestParams like status, you can force it by following the code below.
#RestController
public class ProfileController {
#RequestMapping(value = "/profile/{param}", method = RequestMethod.GET)
public IVRProfile getProfile(#RequestParam(name = "status", required = true) String status, #ModelAttribute("param") String param) {}
}
if you want to force PathVariable, then do this.
#RestController
public class ProfileController {
#RequestMapping(value = "/profile/{param}", method = RequestMethod.GET)
public IVRProfile getProfile(#PathVariable(name = "param", required = true) String param, #ModelAttribute("param") String param) {}
}
Hope this work!!!

Spring Boot JPA - paging and sorting

I am trying to implement pagination to my Spring Data JPA repository in Spring Boot but I am stuck with the following exception when running uni tests:
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.data.domain.Pageable]: Specified class is an interface
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:982)
...
Could someone point out to me what am I missing here? This is my repository:
#Repository
public interface VenueRepository extends PagingAndSortingRepository<Venue, Long> {
public Page<Venue> findAll(Pageable pageable);
}
and controller:
#RestController
#RequestMapping("/venues")
public class VenueController {
#Autowired
private VenueRepository venueRepo;
#RequestMapping(method = RequestMethod.GET)
public ResponseEntity<Page<Venue>> getVenues(Pageable pageable) {
return new ResponseEntity<>(venueRepo.findAll(pageable), HttpStatus.OK);
}
}
and finally my test:
#Test
public void responseOkVenuesTest() throws Exception {
mvc.perform(get("/venues").accept(MediaType.APPLICATION_JSON_VALUE)).andExpect(status().isOk());
}
I spent couple of hours trying to make this work and am running out of ideas. Thank you for any tips!
Change your method getVenues in the way that you can pass the parameters to instantiate a PageRequest instead of passing Pageable :
#RequestMapping(method = RequestMethod.GET)
public ResponseEntity<List<Venue>> getVenues(int from,int to) {
return new ResponseEntity<>(
venueRepo.findAll((new PageRequest(from, to)), HttpStatus.OK).getContent();
}
In addition to #SEY_91's answer you might also like to use the following solution inspired with How to remove redundant Spring MVC method by providing POST-only #Valid? and used in my Spring Boot-driven application for long time.
In short, here is an annotation to annotate controller method parameters:
#Target(PARAMETER)
#Retention(RUNTIME)
public #interface PlainModelAttribute {
}
Now, just a method processor that would scan for parameters annotated with #PlainModelAttribute:
public final class PlainModelAttributeMethodProcessor
extends ModelAttributeMethodProcessor {
private final Map<TypeToken<?>, Converter<? super NativeWebRequest, ?>> index;
private PlainModelAttributeMethodProcessor(final Map<TypeToken<?>, Converter<? super NativeWebRequest, ?>> index) {
super(true);
this.index = index;
}
public static HandlerMethodArgumentResolver plainModelAttributeMethodProcessor(final Map<TypeToken<?>, Converter<? super NativeWebRequest, ?>> index) {
return new PlainModelAttributeMethodProcessor(index);
}
#Override
public boolean supportsParameter(final MethodParameter parameter) {
return parameter.hasParameterAnnotation(PlainModelAttribute.class) || super.supportsParameter(parameter);
}
#Override
protected Object createAttribute(final String attributeName, final MethodParameter parameter, final WebDataBinderFactory binderFactory,
final NativeWebRequest request) {
final TypeToken<?> typeToken = TypeToken.of(parameter.getGenericParameterType());
final Converter<? super NativeWebRequest, ?> converter = index.get(typeToken);
if ( converter == null ) {
throw new IllegalArgumentException("Cannot find a converter for " + typeToken.getType());
}
return converter.convert(request);
}
#Override
protected void bindRequestParameters(final WebDataBinder binder, final NativeWebRequest request) {
final HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
if ( !isSafe(resolve(servletRequest.getMethod())) ) {
((ServletRequestDataBinder) binder).bind(servletRequest);
}
}
private static HttpMethod resolve(final String name) {
return HttpMethod.valueOf(name.toUpperCase());
}
private static boolean isSafe(final HttpMethod method)
throws UnsupportedOperationException {
switch ( method ) {
case GET:
case HEAD:
case OPTIONS:
return true;
case POST:
case PUT:
case PATCH:
case DELETE:
return false;
case TRACE:
throw new UnsupportedOperationException();
default:
throw new AssertionError(method);
}
}
}
I don't really remember, but a resolve() method equivalent should be present in Spring Framework somewhere. Note that I use Google Guava TypeToken in order to let the processor be compatible with generic types (since I use models like IQuery<Foo> and IQuery<Bar> in controllers). Now just register the processor:
#Configuration
#EnableWebMvc
public class MvcConfiguration
extends WebMvcConfigurerAdapter {
#Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(createModelAttributeMethodProcessor());
}
private static HandlerMethodArgumentResolver createModelAttributeMethodProcessor() {
return plainModelAttributeMethodProcessor(ImmutableMap.of(pageableTypeToken, MvcConfiguration::toPageable));
}
private static final TypeToken<Pageable> pageableTypeToken = new TypeToken<Pageable>() {
};
private static Pageable toPageable(final WebRequest request) {
return new PageRequest(
ofNullable(request.getParameter("page")).map(Integer::parseInt).orElse(0),
ofNullable(request.getParameter("size")).map(Integer::parseInt).orElse(1)
);
}
}
Here is a web request to a Pageable DTO conversion, and the converter must be registered as an argument resolver. So now it's ready to use:
#RestController
#RequestMapping("/")
public class Controller {
#RequestMapping(method = GET)
public String get(#PlainModelAttribute final Pageable pageable) {
return toStringHelper(pageable)
.add("offset", pageable.getOffset())
.add("pageNumber", pageable.getPageNumber())
.add("pageSize", pageable.getPageSize())
.add("sort", pageable.getSort())
.toString();
}
}
A few examples:
/ ⇒ PageRequest{offset=0, pageNumber=0, pageSize=1, sort=null}
/?page=43 ⇒ PageRequest{offset=43, pageNumber=43, pageSize=1, sort=null}
/?size=32 ⇒ PageRequest{offset=0, pageNumber=0, pageSize=32, sort=null}
/?page=22&size=32 ⇒ PageRequest{offset=704, pageNumber=22, pageSize=32, sort=null}

Intercept null ResponseBody before marshalling response

I've got multiple controllers for RESTful endpoints which currently return null if there's no resource at the endpoint. For instance,
#RequestMapping(method = ReqeustMethod.GET, value = "{id}")
#ResponseBody
public MyResource get(#PathVariable final Long id) {
return this.myService.get(id); // returns null if bad id
}
I want to return a specific, different resource to the client (ErrorResource) when there's no MyResource with the given id. I know I can do that with a separate method with #ExceptionHandler, such as:
#RequestMapping(method = RequestMethod.GET, value = "{id}")
#ResponseBody
public MyResource get(#PathVariable final Long id) {
final MyResource myResource = this.myService.get(id);
if (myResource == null) {
throw new NotFoundException();
}
return myResource;
}
#ExceptionHandler(NotFoundException.class)
#ResponseStatus(value = HttpStatus.NOT_FOUND)
#ResponseBody
public ErrorResource notFoundException(
final HttpServletRequest request,
final NotFoundException exception) {
final ErrorResource errorResource = new ErrorResource();
errorResource.setStatus(HttpStatus.NOT_FOUND.value());
errorResource.setDeveloperMessage("No resource found at " + request.getRequestURL());
return errorResource;
}
And that's nice. But what I'd really like to be able to do is have some kind of interceptor that figures out for me that whenever an API method is returning a null #ResponseBody, it should instead run the logic in my notFoundException() method. That will make all my controller methods a little cleaner. Is there any way to do that?
It sounds like a job for Spring's HttpMessageConverter.
You can write your own converter by implementing HttpMessageConverter<T> interface.
In your case I would implement a converter of HttpMessageConverter<MyResource> with a null check on the MyResource instance in the write method. If the MyResource instance is null, then build and write your ErrorResource instance.
Here is an example:
import java.io.IOException;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
public class MyResourceConverter implements HttpMessageConverter<MyResource> {
// a real message converter that will respond to ancillary methods and do the actual work
private HttpMessageConverter<Object> delegateConverter;
public MyResourceConverter(HttpMessageConverter<Object> delegateConverter){
this.delegateConverter = delegateConverter;
}
#Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return delegateConverter.canRead(clazz, mediaType) && MyResource.class.equals(clazz);
}
#Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return delegateConverter.canWrite(clazz, mediaType) && MyResource.class.equals(clazz);
}
#Override
public MyResource read(Class<? extends MyResource> clazz,
HttpInputMessage inputMessage) throws IOException,
HttpMessageNotReadableException {
return (MyResource) delegateConverter.read(clazz, inputMessage);
}
#Override
public void write(MyResource t, MediaType contentType,
HttpOutputMessage outputMessage) throws IOException,
HttpMessageNotWritableException {
Object result = null;
if(t == null){
result = // build your ErrorResource here
}else{
result = t;
}
delegateConverter.write(result, contentType, outputMessage);
}
}
Note that this converter needs to be registered in your Spring configuration.
The configuration class must extend WebMvcConfigurerAdapter and override the configureMessageConverters method, like:
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// Here we add our custom-configured HttpMessageConverters.
// yourDelegateConverter may be a MappingJackson2HttpMessageConverter instance for example
converters.add(new EmployeeConverter(yourDelegateConverter));
super.configureMessageConverters(converters);
}
References (from official Spring documentation):
HTTP Message conversion
HttpMessageConverter
WebMvcConfigurerAdapter

How can I use Spring REST with MappingJackson2XmlHttpMessageConverter and XSD validation

I'm creating a Spring 4 REST application, using MappingJackson2XmlHttpMessageConverter to convert incoming XML requests to domain objects. Is there any way to apply XSD validation in that process? If not, I think my fallback is to just make the #RequestBody a String, parse and validate it, and then convert it to the domain object. Is there a better approach?
One approach to this may be to write a custom HttpMessageConverter<T> that checks XSD validation (look here for a way to validate XML with XSD) before returning the object.
Suppose that you have the following method in your Controller class:
#RequestMapping(value = "/")
public CustomObject getCustomObject(#RequestParam(value = "id") String id){
return new CustomObject();
}
Then your converter may look like this:
public class CustomObjectConverter implements HttpMessageConverter<CustomObject> {
// a real message converter that will respond to ancillary methods and do the actual work
protected HttpMessageConverter<Object> delegateConverter;
public CustomObjectConverter (HttpMessageConverter<Object> delegate) {
super(delegate, personService);
super.delegateConverter = delegate;
this.employeePhotoBaseUrl = employeePhotoBaseUrl;
}
#Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return delegateConverter.canRead(clazz, mediaType) && CustomObject.class.equals(clazz);
}
#Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return delegateConverter.canWrite(clazz, mediaType) && CustomObject.class.equals(clazz);
}
#Override
public List<MediaType> getSupportedMediaTypes() {
return delegateConverter.getSupportedMediaTypes();
}
#Override
public CustomObject read(Class<? extends CustomObject> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return (CustomObject) delegateConverter.read(clazz, inputMessage);
}
#Override
public void write(CustomObject t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
if(validationOK)
delegateConverter.write(t, contentType, outputMessage);
else
// You may implement a custom exception handler to return a proper HTTP error code
throw new YourCustomException();
}
}
Remember to configure your new converter. I do this in my configuration class:
#Configuration
#EnableWebMvc
public class RestConfig extends WebMvcConfigurerAdapter {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// initialize your MappingJackson2XmlHttpMessageConverter
MappingJackson2XmlHttpMessageConverter xmlMessageConverter = xmlMessageConverter();
//Here we add our custom-configured HttpMessageConverters
converters.add(new CustomObjectConverter(xmlMessageConverter));
super.configureMessageConverters(converters);
}
}

Spring Data Pagination returns no results with JSONView

I am using Spring data pagination in my REST Controller and returning Paged entity. I would like to control the data returned as JSON with the help of JSONViews.
I am able to achieve the result when I return a single object. But when I return Page, I am receiving blank JSON as response.
Following is my method signature.
#JsonView(TravelRequestView.MyRequests.class)
#RequestMapping("/travel/requests")
public Page<TravelRequest> getUserTravelRequests(
#RequestParam("ps") int pageSize, #RequestParam("p") int page,
#RequestParam(defaultValue = "", value = "q") String searchString)
I am able to receive response when I remove #JsonView annotation.
If you are using spring-boot, then another simpler solution would be add the following to application.yml
spring:
jackson:
mapper:
DEFAULT_VIEW_INCLUSION: true
or application.properties
spring.jackson.mapper.DEFAULT_VIEW_INCLUSION=true
With this approach, we have the advantage of retaining the ObjectMapper managed by Spring Container and not creating a new ObjectMapper. So once we use the spring managed ObjectMapper, then any Custom Serialzers we define will still continue to work e.g CustomDateSerialzer
Reference: http://docs.spring.io/spring-boot/docs/current/reference/html/howto-spring-mvc.html
Setting DEFAULT_VIEW_INCLUSION has a global effect, while all we need is to be able to serialize a Page object. The following code will register a serializer for Page, and is a simple change to your code:
#Bean
public Module springDataPageModule() {
return new SimpleModule().addSerializer(Page.class, new JsonSerializer<Page>() {
#Override
public void serialize(Page value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
gen.writeNumberField("totalElements",value.getTotalElements());
gen.writeNumberField("totalPages", value.getTotalPages());
gen.writeNumberField("number", value.getNumber());
gen.writeNumberField("size", value.getSize());
gen.writeBooleanField("first", value.isFirst());
gen.writeBooleanField("last", value.isLast());
gen.writeFieldName("content");
serializers.defaultSerializeValue(value.getContent(),gen);
gen.writeEndObject();
}
});
}
Another (arguably more elegant) solution is to register the following ResponseBodyAdvice. It will make sure your REST endpoint will still return a JSON array, and set a HTTP header 'X-Has-Next-Page' to indicate whether there is more data. The advantages are:
1) No extra count(*) query to your DB (single query)
2) Response is more elegant, since it returns a JSON array
/**
* ResponseBodyAdvice to support Spring data Slice object in JSON responses.
* If the value is a slice, we'll write the List as an array, and add a header to the HTTP response
*
* #author blagerweij
*/
#ControllerAdvice
public class SliceResponseBodyAdvice implements ResponseBodyAdvice<Object> {
#Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Slice) {
Slice slice = (Slice) body;
response.getHeaders().add("X-Has-Next-Page", String.valueOf(slice.hasNext()));
return slice.getContent();
}
return body;
}
}
Try with below piece of code,
#Configuration
public class MyInterceptorConfig extends WebMvcConfigurerAdapter{
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper mapper = new ObjectMapper() {
private static final long serialVersionUID = 1L;
#Override
protected DefaultSerializerProvider _serializerProvider(SerializationConfig config) {
// replace the configuration with my modified configuration.
// calling "withView" should keep previous config and just add my changes.
return super._serializerProvider(config.withView(TravelRequestView.MyRequests.class));
}
};
mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
converter.setObjectMapper(mapper);
converters.add(converter);
}
Although I don't want to take credit for this,
It was a reference from
Jackson JsonView not being applied
It would retrieve all the variables of an entity which are annotated with jsonview (TravelRequestView.MyRequests.class) along with all the variables which are not annotated with jsonview. If you don't want certain properties of an object, annotated with different view.
I actually found a simpler and better way of doing this. The problem I had was with the fact that I cannot set #JsonView annotations on the Page object I was receiving. So I created an implementation of the Page interface and added my JsonViews to it. And instead of returning Page, I am now returning MyPage
public class MyPage<T> implements Page<T> {
private Page<T> pageObj;
public MyPage(Page<T> pageObj) {
this.pageObj = pageObj;
}
#JsonView(PaginatedResult.class)
#Override
public int getNumber() {
return pageObj.getNumber();
}
#Override
public int getSize() {
return pageObj.getSize();
}
#JsonView(PaginatedResult.class)
#Override
public int getNumberOfElements() {
return pageObj.getNumberOfElements();
}
#JsonView(PaginatedResult.class)
#Override
public List<T> getContent() {
return pageObj.getContent();
}
#Override
public boolean hasContent() {
return pageObj.hasContent();
}
#Override
public Sort getSort() {
return pageObj.getSort();
}
#JsonView(PaginatedResult.class)
#Override
public boolean isFirst() {
return pageObj.isFirst();
}
#JsonView(PaginatedResult.class)
#Override
public boolean isLast() {
return pageObj.isLast();
}
#Override
public boolean hasNext() {
return pageObj.hasNext();
}
#Override
public boolean hasPrevious() {
return pageObj.hasPrevious();
}
#Override
public Pageable nextPageable() {
return pageObj.nextPageable();
}
#Override
public Pageable previousPageable() {
return pageObj.previousPageable();
}
#Override
public Iterator<T> iterator() {
return pageObj.iterator();
}
#JsonView(PaginatedResult.class)
#Override
public int getTotalPages() {
return pageObj.getTotalPages();
}
#JsonView(PaginatedResult.class)
#Override
public long getTotalElements() {
return pageObj.getTotalElements();
}
}
you need to add annotation #JsonView(TravelRequestView.MyRequests.class) recursively. Add it to the field you want to see in Page class.
public class Page<T> {
#JsonView(TravelRequestView.MyRequests.class)
private T view;
...
}
or enable DEFAULT_VIEW_INCLUSION for ObjectMapper:
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter" />
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper">
<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
<property name="defaultViewInclusion" value="true"/>
</bean>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
or use dto objects for your responses where you can control all your views
Still cleaner is to tell Jackson to Serialize all props for a bean of type Page.
To do that, you just have to declare to adapt slighly the Jackson BeanSerializer : com.fasterxml.jackson.databind.ser.BeanSerializer Create a class that extends that BeanSerializer called PageSerialier and if the bean is of type Page<> DO NOT apply the property filtering.
As show in the code below, i just removed the filtering for Page instances :
public class MyPageSerializer extends BeanSerializer {
/**
* MODIFIED By Gauthier PEEL
*/
#Override
protected void serializeFields(Object bean, JsonGenerator gen, SerializerProvider provider)
throws IOException, JsonGenerationException {
final BeanPropertyWriter[] props;
// ADDED
// ADDED
// ADDED
if (bean instanceof Page) {
// for Page DO NOT filter anything so that #JsonView is passthrough at this level
props = _props;
} else {
// ADDED
// ADDED
if (_filteredProps != null && provider.getActiveView() != null) {
props = _filteredProps;
} else {
props = _props;
}
}
// rest of the method unchanged
}
// inherited constructor removed for concision
}
Then you need to declare it to Jackson with a Module :
public class MyPageModule extends SimpleModule {
#Override
public void setupModule(SetupContext context) {
context.addBeanSerializerModifier(new BeanSerializerModifier() {
#Override
public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc,
JsonSerializer<?> serializer) {
if (serializer instanceof BeanSerializerBase) {
return new MyPageSerializer ((BeanSerializerBase) serializer);
}
return serializer;
}
});
}
}
Spring conf now : in your #Configuration create a new #Bean of the MyPageModule
#Configuration
public class WebConfigPage extends WebMvcConfigurerAdapter {
/**
* To enable Jackson #JsonView to work with Page<T>
*/
#Bean
public MyPageModule myPageModule() {
return new MyPageModule();
}
}
And you are done.

Categories