#RequestHeader not binding in POJO but binding only in variable - java

I am not able to bind the request header values into a POJO class. Here is an explanation:
I want to bind the value of "isKidsProfile" into "DetailCO" but it is not binding. On the other hand, it is working if I am binding it into a variable only.
// consider header value in request is: key:isKidsProfile and value:true/false
#RequestMapping(value = "/api/v1/detail/{id}", method = RequestMethod.GET)
public ResponseDTO fetchDetailForKidsProfileUser(
#RequestHeader DetailCO detailCO,
#RequestHeader boolean isKidsProfile) {
sout(detailCO.isKidsProfile); // not bind in object
sout(isKidsProfile); // bind in variable
return new ResponseDTO();
}
class DetailCO {
private boolean isKidsProfile;
//getters ans setters
}
There are more values so it will be good to bind in POJO rather than creating multiple variables. Please suggest.

This is what I have used for my use case where I needed to parse all Parameters. You may use RequestHeaderMethodArgumentResolver if it's just the headers.
Create a configuration
#Configuration
public class IRSConfig implements WebMvcConfigurer {
#Autowired
private IRSArgumentResolver irsArgumentResolver;
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(irsArgumentResolver);
}
}
Create POJO class for encapsulating your data and sending to RequestHandler
public class MyRequestParams {
private String first;
private String second;
public void setFirst(String first) {
this.first = first;
}
public void setSecond(String second) {
this.second = second;
}
public String getFirst() {
return first;
}
public String getSecond() {
return second;
}
}
Create an argument resolver
#Component
public final class IRSArgumentResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterType().equals(MyRequestParams.class);
}
#Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
MyRequestParams requestParams = new MyRequestParams();
requestParams.setFirst(nativeWebRequest.getParameter("x-et-participant-id"));
requestParams.setSecond(nativeWebRequest.getHeader("Authorization"));
return requestParams;
}
}

You should be able to register a Converter<String, DeailCO>.

Related

Parse RequestBody as two different objects in Spring boot

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) {
}
}

Spring filtering response fields with jackson

I am trying to filter fields in a nested object:
class Response {
// These objects themselves can have many fields within
private final PropA a;
private final PropB b;
#JsonCreator
public Response(PropA a, PropB b) { ... }
}
I'd like a generic 'filter helper' to achieve the above logic. Here is what I have so far (following a similar approach as this project)
public class FilterHelper {
private final ObjectMapper objectMapper;
public FilterHelper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.objectMapper.addMixIn(Object.class, MyFilterMixin.class);
}
#JsonFilter("myfilter")
public static class MyFilterMixin {
}
private static class MyFilter extends SimpleBeanPropertyFilter {
private final Set<String> properties;
public MyFilter(Set<String> properties) {
super();
this.properties = properties;
}
#Override
public void serializeAsField(final Object pojo, final JsonGenerator jgen, final SerializerProvider provider,
final PropertyWriter writer) throws Exception {
System.out.println("************************** " + writer.getName());
if (properties.contains(writer.getName())) {
writer.serializeAsField(pojo, jgen, provider);
} else if (!jgen.canOmitFields()) {
writer.serializeAsOmittedField(pojo, jgen, provider);
}
}
}
public String filter(T obj, Set<String> fields) {
FilterProvider filterProvider = new SimpleFilterProvider().addFilter("myfilter", new MyFilter(fields));
return objectMapper.writer(filterProvider).writeValueAsString(obj);
}
}
When I hit this endpoint with ?fields=one,two as query parameter I expect to see from a line printed to console for every field within that top level Response object as follows:
******************* a
******************* a1
******************* a2
******************* ..etc
******************* b
******************* b1
******************* b2
******************* ..etc
but I am only seeing output for the top level a and b fields followed by an error before getting a 500 status code from the endpoint:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot resolve PropertyFilter with id 'myfilter'; no FilterProvider configured (through reference chain: com.google.common.collect.SingletonImmutableList[0])
It is worth mentioning that I had this working somehow, but it was broken after some changes I don't recall.
Unless you need to provide custom serialization for different fields, you should not be hooking the serializeAsField and instead you should be overriding the #include variant methods:
com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter#include(com.fasterxml.jackson.databind.ser.BeanPropertyWriter)
com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter#include(com.fasterxml.jackson.databind.ser.PropertyWriter)
as follows:
private static class MyFilter extends SimpleBeanPropertyFilter {
private final Set<String> properties;
public MyFilter(Set<String> properties) {
super();
this.properties = properties;
}
#Override
protected boolean include(BeanPropertyWriter writer) {
return !this.properties.contains(writer.getName());
}
#Override
protected boolean include(PropertyWriter writer) {
return !this.properties.contains(writer.getName());
}
}
There is even a static factory providing a com.fasterxml.jackson.databind.ser.PropertyFilter that filters out a specific set of fields:
com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter#serializeAllExcept(java.util.Set<java.lang.String>)
Extra issue
At the filter helper level, you are serializing the filtered object to JSON then deserializing it back (with filtered fields) to an object that you are handing back as the endpoint response.
Solution / Alternative
You can simply omit the intermediary step by just sterilizing the result Response with the filter fields predicate and returning the result JSON as ResponseEntity:
FilterHelper:
#Component
public class FilterHelper {
private final ObjectMapper objectMapper;
#Autowired
public FilterHelper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.objectMapper.addMixIn(Object.class, MyFilterMixin.class);
}
#JsonFilter("myfilter")
public static class MyFilterMixin {
}
private static class MyFilter extends SimpleBeanPropertyFilter {
private final Set<String> properties;
public MyFilter(Set<String> properties) {
super();
this.properties = properties;
}
#Override
protected boolean include(BeanPropertyWriter writer) {
return !this.properties.contains(writer.getName());
}
#Override
protected boolean include(PropertyWriter writer) {
return !this.properties.contains(writer.getName());
}
}
public String filter(Object dto, Set<String> fields) {
if (fields == null || fields.isEmpty()) {
return "";
}
FilterProvider filterProvider = new SimpleFilterProvider()
.addFilter("myfilter", SimpleBeanPropertyFilter.serializeAllExcept(fields));
try {
return objectMapper.writer(filterProvider).writeValueAsString(dto);
} catch (JsonProcessingException e) {
return "";
}
}
}
Controller:
#GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseStatus(OK)
ReponseEntity<String> someEndpoint(#RequestParam(name = "fields") Set<String> fields) {
Response response = getResponseFromSomewhere();
return ResponseEntity.ok(filterHelper.filter(response, fields));
}

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}

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.

Jackson - custom serializer that overrides only specific fields

I know how to use a custom serializer in Jackson (by extending JsonSerializer), but I want the default serializer to work for all fields, except for just 1 field, which I want to override using the custom serializer.
Annotations are not an option, because I am serializing a generated class (from Thrift).
How do I specify only certain fields to be overridden when writing a custom jackson serializer?
Update:
Here's the class I want to serialize:
class Student {
int age;
String firstName;
String lastName;
double average;
int numSubjects
// .. more such properties ...
}
The above class has many properies, most of which use native types. I want to just override a few properties in the custom serializer and let Jackson deal with the rest as usual. For e.g. I just want to convert the "age" field to a custom output.
Assuming your Target class is
public class Student {
int age;
String firstName;
String lastName;
double average;
int numSubjects;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public double getAverage() {
return average;
}
public void setAverage(double average) {
this.average = average;
}
public int getNumSubjects() {
return numSubjects;
}
public void setNumSubjects(int numSubjects) {
this.numSubjects = numSubjects;
}
}
You need to write a custom serializer as given below
public class MyCustomSerializer extends JsonSerializer<Student> {
#Override
public void serialize(Student value, JsonGenerator jgen,
SerializerProvider provider) throws IOException,
JsonProcessingException {
if (value != null) {
jgen.writeStartObject();
jgen.writeStringField("age", "Age: " + value.getAge()); //Here a custom way to render age field is used
jgen.writeStringField("firstName", value.getFirstName());
jgen.writeStringField("lastName", value.getLastName());
jgen.writeNumberField("average", value.getAverage());
jgen.writeNumberField("numSubjects", value.getNumSubjects());
//Write other properties
jgen.writeEndObject();
}
}
}
then add it to the ObjectMapper
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule("custom",
Version.unknownVersion());
module.addSerializer(Student.class, new MyCustomSerializer());
mapper.registerModule(module);
then use it like
Student s = new Student();
s.setAge(2);
s.setAverage(3.4);
s.setFirstName("first");
s.setLastName("last");
s.setNumSubjects(3);
StringWriter sw = new StringWriter();
mapper.writeValue(sw, s);
System.out.println(sw.toString());
It will produce a o/p like
{"age":"Age:
2","firstName":"first","lastName":"last","average":3.4,"numSubjects":3}
Just because you can not modify classes DOES NOT mean you could not use annotations: just use mix-in annotations. See this blog entry for example (or google for more with "jackson mixin annotations") for how to use this.
I have specifically used Jackson with protobuf- and thrift-generated classes, and they work pretty well. For earlier Thrift versions, I had to disable discovery of "is-setters", methods Thrift generates to see if a specific property has been explicitly set, but otherwise things worked fine.
I faced the same issue, and I solved it with CustomSerializerFactory.
This approach allows you to ignore some specific field for either for all objects, or for specific types.
public class EntityCustomSerializationFactory extends CustomSerializerFactory {
//ignored fields
private static final Set<String> IGNORED_FIELDS = new HashSet<String>(
Arrays.asList(
"class",
"value",
"some"
)
);
public EntityCustomSerializationFactory() {
super();
}
public EntityCustomSerializationFactory(Config config) {
super(config);
}
#Override
protected void processViews(SerializationConfig config, BeanSerializerBuilder builder) {
super.processViews(config, builder);
//ignore fields only for concrete class
//note, that you can avoid or change this check
if (builder.getBeanDescription().getBeanClass().equals(Entity.class)){
//get original writer
List<BeanPropertyWriter> originalWriters = builder.getProperties();
//create actual writers
List<BeanPropertyWriter> writers = new ArrayList<BeanPropertyWriter>();
for (BeanPropertyWriter writer: originalWriters){
String propName = writer.getName();
//if it isn't ignored field, add to actual writers list
if (!IGNORED_FIELDS.contains(propName)){
writers.add(writer);
}
}
builder.setProperties(writers);
}
}
}
And afterwards you can use it something like the following:
objectMapper.setSerializerFactory(new EntityCustomSerializationFactory());
objectMapper.writeValueAsString(new Entity());//response will be without ignored fields
In case you don't want to pollute your model with annotations, you could use mixins.
ObjectMapper mapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.setMixInAnnotation(Student.class, StudentMixin.class);
mapper.registerModule(simpleModule);
And you want to override id field for example:
public abstract class StudentMixin {
#JsonSerialize(using = StudentIdSerializer.class)
public String id;
}
Do whatever you need with the field:
public class StudentIdSerializer extends JsonSerializer<Integer> {
#Override
public void serialize(Integer integer, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(String.valueOf(integer * 2));
}
}
with the help of #JsonView we can decide fields of model classes to serialize which satisfy the minimal criteria ( we have to define the criteria) like we can have one core class with 10 properties but only 5 properties can be serialize which are needful for client only
Define our Views by simply creating following class:
public class Views
{
static class Android{};
static class IOS{};
static class Web{};
}
Annotated model class with views:
public class Demo
{
public Demo()
{
}
#JsonView(Views.IOS.class)
private String iosField;
#JsonView(Views.Android.class)
private String androidField;
#JsonView(Views.Web.class)
private String webField;
// getters/setters
...
..
}
Now we have to write custom json converter by simply extending HttpMessageConverter class from spring as:
public class CustomJacksonConverter implements HttpMessageConverter<Object>
{
public CustomJacksonConverter()
{
super();
//this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView(Views.ClientView.class));
this.delegate.getObjectMapper().configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
this.delegate.getObjectMapper().setSerializationInclusion(Include.NON_NULL);
}
// a real message converter that will respond to methods and do the actual work
private MappingJackson2HttpMessageConverter delegate = new MappingJackson2HttpMessageConverter();
#Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return delegate.canRead(clazz, mediaType);
}
#Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return delegate.canWrite(clazz, mediaType);
}
#Override
public List<MediaType> getSupportedMediaTypes() {
return delegate.getSupportedMediaTypes();
}
#Override
public Object read(Class<? extends Object> clazz,
HttpInputMessage inputMessage) throws IOException,
HttpMessageNotReadableException {
return delegate.read(clazz, inputMessage);
}
#Override
public void write(Object obj, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException
{
synchronized(this)
{
String userAgent = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getHeader("userAgent");
if ( userAgent != null )
{
switch (userAgent)
{
case "IOS" :
this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView(Views.IOS.class));
break;
case "Android" :
this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView(Views.Android.class));
break;
case "Web" :
this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView( Views.Web.class));
break;
default:
this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView( null ));
break;
}
}
else
{
// reset to default view
this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView( null ));
}
delegate.write(obj, contentType, outputMessage);
}
}
}
Now there is need to tell spring to use this custom json convert by simply putting this in dispatcher-servlet.xml
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean id="jsonConverter" class="com.mactores.org.CustomJacksonConverter" >
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
That's how you will able to decide which fields to get serialize.
Thanx

Categories