java spring MappingJacksonJsonView not doing toString on mongodb ObjectId - java

I am using the MappingJacksonJsonView in my SpringMVC application to render JSON from my controllers. I want the ObjectId from my object to render as .toString but instead it serializes the ObjectId into its parts. It works just fine in my Velocity/JSP pages:
Velocity:
$thing.id
Produces:
4f1d77bb3a13870ff0783c25
Json:
<script type="text/javascript">
$.ajax({
type: 'GET',
url: '/things/show/4f1d77bb3a13870ff0783c25',
dataType: 'json',
success : function(data) {
alert(data);
}
});
</script>
Produces:
thing: {id:{time:1327331259000, new:false, machine:974358287, timeSecond:1327331259, inc:-260555739},…}
id: {time:1327331259000, new:false, machine:974358287, timeSecond:1327331259, inc:-260555739}
inc: -260555739
machine: 974358287
new: false
time: 1327331259000
timeSecond: 1327331259
name: "Stack Overflow"
XML:
<script type="text/javascript">
$.ajax({
type: 'GET',
url: '/things/show/4f1d77bb3a13870ff0783c25',
dataType: 'xml',
success : function(data) {
alert(data);
}
});
</script>
Produces:
<com.place.model.Thing>
<id>
<__time>1327331259</__time>
<__machine>974358287</__machine>
<__inc>-260555739</__inc>
<__new>false</__new>
</id>
<name>Stack Overflow</name>
</com.place.model.Thing>
Is there a way to stop MappingJacksonJsonView from getting that much information out of the ObjectId? I just want the .toString() method, not all the details.
Thanks.
Adding the Spring config:
#Configuration
#EnableWebMvc
public class MyConfiguration {
#Bean(name = "viewResolver")
public ContentNegotiatingViewResolver viewResolver() {
ContentNegotiatingViewResolver contentNegotiatingViewResolver = new ContentNegotiatingViewResolver();
contentNegotiatingViewResolver.setOrder(1);
contentNegotiatingViewResolver.setFavorPathExtension(true);
contentNegotiatingViewResolver.setFavorParameter(true);
contentNegotiatingViewResolver.setIgnoreAcceptHeader(false);
Map<String, String> mediaTypes = new HashMap<String, String>();
mediaTypes.put("json", "application/x-json");
mediaTypes.put("json", "text/json");
mediaTypes.put("json", "text/x-json");
mediaTypes.put("json", "application/json");
mediaTypes.put("xml", "text/xml");
mediaTypes.put("xml", "application/xml");
contentNegotiatingViewResolver.setMediaTypes(mediaTypes);
List<View> defaultViews = new ArrayList<View>();
defaultViews.add(xmlView());
defaultViews.add(jsonView());
contentNegotiatingViewResolver.setDefaultViews(defaultViews);
return contentNegotiatingViewResolver;
}
#Bean(name = "xStreamMarshaller")
public XStreamMarshaller xStreamMarshaller() {
return new XStreamMarshaller();
}
#Bean(name = "xmlView")
public MarshallingView xmlView() {
MarshallingView marshallingView = new MarshallingView(xStreamMarshaller());
marshallingView.setContentType("application/xml");
return marshallingView;
}
#Bean(name = "jsonView")
public MappingJacksonJsonView jsonView() {
MappingJacksonJsonView mappingJacksonJsonView = new MappingJacksonJsonView();
mappingJacksonJsonView.setContentType("application/json");
return mappingJacksonJsonView;
}
}
And my controller:
#Controller
#RequestMapping(value = { "/things" })
public class ThingController {
#Autowired
private ThingRepository thingRepository;
#RequestMapping(value = { "/show/{thingId}" }, method = RequestMethod.GET)
public String show(#PathVariable ObjectId thingId, Model model) {
model.addAttribute("thing", thingRepository.findOne(thingId));
return "things/show";
}
}

By default Jackson provides the serialization of Object received. ObjectId returns the Object therefor its attributes are visible after conversion to JSON. You need to specify the type of serialization required, Here in this case it is string. Thing entity class which is used to create ThingRepository will look like this to get this done:
public class Thing {
#Id
#JsonSerialize(using= ToStringSerializer.class)
ObjectId id;
String name;
}
Here make a note of added anotation #JsonSerialize(using= ToStringSerializer.class) which instructs to serialize the ObjectID to String.

Previous answer did the trick, but it was ugly and not well thought out - a clear workaround to actually fixing the problem.
The real issue is that ObjectId deserializes into its component parts. MappingJacksonJsonView sees ObjectId for what it is, an object, and goes to work on it. The deserialized fields being seen in the JSON are the fields that make up an ObjectId. To stop the serialization/deserialization of such an object, you have to configure a CustomObjectMapper that extends ObjectMapper.
Here is the CustomeObjectMapper:
public class CustomObjectMapper extends ObjectMapper {
public CustomObjectMapper() {
CustomSerializerFactory sf = new CustomSerializerFactory();
sf.addSpecificMapping(ObjectId.class, new ObjectIdSerializer());
this.setSerializerFactory(sf);
}
}
Here is the ObjectIdSerializer that the CustomObjectMapper uses:
public class ObjectIdSerializer extends SerializerBase<ObjectId> {
protected ObjectIdSerializer(Class<ObjectId> t) {
super(t);
}
public ObjectIdSerializer() {
this(ObjectId.class);
}
#Override
public void serialize(ObjectId value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
jgen.writeString(value.toString());
}
}
And here is what needs to change in your #Configuration-annotated class:
#Bean(name = "jsonView")
public MappingJacksonJsonView jsonView() {
final MappingJacksonJsonView mappingJacksonJsonView = new MappingJacksonJsonView();
mappingJacksonJsonView.setContentType("application/json");
mappingJacksonJsonView.setObjectMapper(new CustomObjectMapper());
return mappingJacksonJsonView;
}
You are basically telling Jackson how to serialize/deserialize this particular object. Works like a charm.

If you're using autowired instance of the auto configured mapper in Spring Boot, you can just add this customizer bean:
#Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
return builder -> builder.serializerByType(ObjectId.class, ToStringSerializer.instance);
}
Relevant imports:
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import org.bson.types.ObjectId;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
And then this will reflect anywhere the autowired mapper is used, for example:
#Service
public class MyService {
private final ObjectMapper objectMapper;
private final MongoTemplate mongoTemplate;
#Autowired
public MyService(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public String getJsonForMongoCommand(Document document) {
return objectMapper.writeValueAsString(mongoTemplate.executeCommand(document));
}
}
Or in this specific case (untested, might be unnecessary):
#Bean(name = "jsonView")
public MappingJacksonJsonView jsonView(ObjectMapper objectMapper) {
final MappingJacksonJsonView mappingJacksonJsonView = new MappingJacksonJsonView();
mappingJacksonJsonView.setContentType("application/json");
mappingJacksonJsonView.setObjectMapper(objectMapper);
return mappingJacksonJsonView;
}

I had to just make the getId() method return a String. It was the only way to make Jackson stop serializing the ObjectId.
public String getId() {
if (id != null) {
return id.toString();
} else {
return null;
}
}
public void setId(ObjectId id) {
this.id = id;
}
setId() still has to be ObjectId so Mongo (and its driver) can set the ID correctly.

Just to complement the answers, if you face a scenario where you also need to serialize an array of ObjectId, you could create a custom serializer with the following logic:
public class ObjectIdSerializer extends JsonSerializer<Object> {
#Override
public void serialize(final Object value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException {
if (value instanceof Collection) {
final Collection<String> ids = new ArrayList<>();
for (final Object id : ((Collection<?>) value)) {
ids.add(ObjectId.class.cast(id).toString());
}
jgen.writeObject(ids);
} else {
jgen.writeString(ObjectId.class.cast(value).toString());
}
}
}

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 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.

Restlet Complex Object to XML serializaton

I have restlet web service which returns response as xml. I'm using Jackson as binder.
below is class I'm returning.
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class ApiResponse<T> implements Serializable {
/**
*
*/
private static final long serialVersionUID = -2736991050157565598L;
private int responseCode;
private String reponseMessage;
private List<T> body = new ArrayList<T>();
public int getResponseCode() {
return responseCode;
}
public void setResponseCode(int responseCode) {
this.responseCode = responseCode;
}
public String getReponseMessage() {
return reponseMessage;
}
public void setReponseMessage(String reponseMessage) {
this.reponseMessage = reponseMessage;
}
public List<T> getBody() {
return body;
}
public void setBody(List<T> body) {
this.body = body;
}
}
And below is response of the service. Everything is almost good except that it puts as property names for nested objects the same as parents. It shows body for nested tag names but I expect it to be T template. Any ideas?
<ApiResponse>
<responseCode>1</responseCode>
<reponseMessage />
<body>
<body>
<reportId>1</reportId>
<reportName>name1</reportName>
</body>
<body>
<reportId>2</reportId>
<reportName>name2</reportName>
</body>
</body>
</ApiResponse>
This is the default serialization with Jackson. However you can leverage custom serializer to improve this. This feature allows you to have the hand on the generated content within Jackson for a specific class. You can override the default strategy with your own and configure in a very fine manner what will be created.
Below a sample of such entity that generates content for the class SomeBean:
public class SomeBeanSerializer extends JsonSerializer<SomeBean> {
#Override
public void serialize(SomeBean bean, JsonGenerator jgen,
SerializerProvider provider) throws IOException,
JsonProcessingException {
jgen.writeStartObject();
// Fields
jgen.writeNumberField("id", bean.getId());
(...)
// Link
String href = (...)
HypermediaLink linkToSelf = new HypermediaLink();
linkToSelf.setHref(href + bean.getId());
linkToSelf.setRel("self");
jgen.writeObjectField("hypermediaLink", linkToSelf);
jgen.writeEndObject();
}
}
Here is the way to configure this within Restlet:
JacksonConverter jacksonConverter = getRegisteredJacksonConverter();
if (jacksonConverter != null) {
ObjectMapper objectMapper = jacksonConverter.getObjectMapper();
SimpleModule module = new SimpleModule("MyModule", new Version(1, 0, 0, null));
module.addSerializer(SomeBean.class, new SomeBeanSerializer());
objectMapper.registerModule(module);
}
This link could help you to see how to configure the Jackson converter of Restlet: https://templth.wordpress.com/2015/02/23/optimizing-restlet-server-applications/. It provides the content of the method getRegisteredJacksonConverter.
Edited: with version 2.3 of Restlet, something changes at this level. The object mapper is now brought by the JacksonRepresentation instead of the JacksonConverter itself. The object mapper is now instantiated for each representation of this kind. This means that you need to sub class these two elements to configure the custom serializer.
Here is the code of the class CustomJacksonRepresentation:
public class CustomJacksonRepresentation<T>
extends JacksonRepresentation<T> {
#Override
public ObjectMapper getObjectMapper() {
if (this.objectMapper == null) {
this.objectMapper = createObjectMapper();
SimpleModule module = new SimpleModule("MyModule",
new Version(1, 0, 0, null));
module.addSerializer(SomeBean.class,
new SomeBeanSerializer());
objectMapper.registerModule(module);
}
return this.objectMapper;
}
}
Here is the code of the class CustomJacksonConverter:
public class CustomJacksonConverter
extends JacksonConverter {
protected <T> JacksonRepresentation<T> create(
MediaType mediaType, T source) {
return new CustomJacksonRepresentation<T>(
mediaType, source);
}
protected <T> JacksonRepresentation<T> create(
Representation source, Class<T> objectClass) {
return new CustomJacksonRepresentation<T>(
source, objectClass);
}
}
This implemented, you need to replace the existing jackson converter that is automatically registered by Restlet. Here is the code to do that:
// Looking for the registered jackson converter
JacksonConverter jacksonConverter = null;
List<ConverterHelper> converters
= Engine.getInstance().getRegisteredConverters();
for (ConverterHelper converterHelper : converters) {
if (converterHelper instanceof JacksonConverter) {
jacksonConverter = (JacksonConverter) converterHelper;
break;
}
}
// converters
Engine.getInstance().getRegisteredConverters().remove(
jacksonConverter);
CustomJacksonConverter customJacksonConverter
= new CustomJacksonConverter();
Engine.getInstance().getRegisteredConverters().add(
customJacksonConverter);
You can notice that the way to manage converters will be refactored in the version 3 of Restlet to make things more convenient to configure! ;-)
Hope it helps you,
Thierry

Jackson, move dynamic property value one level up

I have problem with modelling server responses, some of them look like that:
{
"_links":{
"self":{
"href":"http:\/\/example.com"
}
},
"_embedded":{
"category":{
<...data...>
}
}
}
or
{
"_links":{
"self":{
"href":"http:\/\/example.com"
}
},
"_embedded":{
"episodes":[
<...list_data...>
]
}
}
It seems that "_embedded" property has only one JSON object and that object has only one property ( named differently ) with actual data.
I would like to create some kind of generic POJO class to support those kind of responses, something like:
public abstract class EmbeddedResponse<T> {
#JsonProperty("_embedded")
private T embedded;
public T getEmbedded() {
return embedded;
}
... <other_members> ...
}
public class CategoriesResponse extends EmbeddedResponse<List<Category>> {
}
Where calling 'getEmbedded()' would return list of categories ( or episodes, or anything ).
I am working with custom deserialization now, but without much success, I would like to keep code base minimal.
Solution, abstract POJO class:
public class EmbeddedResponse<T> {
#JsonProperty("_embedded")
#JsonDeserialize( using = EmbeddedResponseDeserializer.class )
private T embedded;
public T getEmbedded() {
return embedded;
}
}
POJO for actual response:
public class CategoriesResponse extends EmbeddedResponse<List<Category>> {
}
Deserializer for JSON in question:
public class EmbeddedResponseDeserializer extends JsonDeserializer<Object> implements ContextualDeserializer {
private JavaType javaType;
#Override
public Object deserialize( JsonParser jsonParser, DeserializationContext ctxt ) throws IOException {
ObjectCodec objectCodec = jsonParser.getCodec();
JsonNode node = objectCodec.readTree(jsonParser);
// Get first it might require correction
String fieldName = node.fieldNames().next();
JsonNode skippedNode = node.get( fieldName );
return objectCodec.readValue( skippedNode.traverse(), javaType );
}
#Override
public JsonDeserializer<?> createContextual( DeserializationContext ctxt, BeanProperty property ) throws JsonMappingException {
javaType = property.getType();
return this;
}
}
It might require more tweeks but at this point this solution is working
I would use the Java 8 Optional object when modelling the objects. This way you get a flexible model and nice programming model by e.g. using the ifPresent-method.
So, the root class could be modelled along the lines of:
public class Response {
private Embedded embedded;
private Links links;
#JsonCreator
public Response(
#JsonProperty("_links") final Links links,
#JsonProperty("_embedded") final Embedded embedded) {
this.links = links;
this.embedded = embedded;
}
public Embedded embedded() {
return embedded;
}
public Links links() {
return links;
}
}
The object that defines the embedded content (i.e. category or episodes) could be modelled like this:
public class Embedded {
private final Category category;
private final List<Episode> episodes;
#JsonCreator
public Embedded(
#JsonProperty("episodes") final List<Episode> episodes,
#JsonProperty("category") final Category category) {
this.episodes = episodes;
this.category = category;
}
public Optional<Category> category() {
return Optional.ofNullable(category);
}
public Optional<List<Episode>> episodes() {
return Optional.ofNullable(episodes);
}
}
When programming towards these objects the following pattern could be used:
final InputStream resource = ...; // retrieve a stream somehow
// Map the stream to the response object
final Response response = new ObjectMapper().readValue(resource, Response.class);
// Use the Optional-style for processing category data
response.embedded().category().ifPresent(category -> {
// do category stuff with the Category-object
});
// Once more, use the Optional-style - this time for processing episodes data
response.embedded().episodes().ifPresent(episodes -> {
// do episodes stuff with the List of Episodes
});

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