I'm getting something like this in my JSON response (I'm having a REST implementation in SpringBoot):
"estimatedDeliveryTimeWindow":{
"window":{}
}
I have set custom HTTPMessageCOnverters and configured objectMapper like this:
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
Also tried to remove default converters using below code:
#Bean
public HttpMessageConverters converters() {
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
jsonConverter.setObjectMapper(objectMapper);
return new HttpMessageConverters(false, Arrays.asList(jsonConverter));
}
Nothing seems to work. I still see null objects within objects. These objects are complex objects nested with primitive types and custom objects. What else I can try?
Please add #JsonInclude(Include.NON_NULL) before the class files
#JsonInclude(Include.NON_NULL)
public class MobileLoginVO {
private String otpDetailsId;
public String getOtpDetailsId() {
return otpDetailsId;
}
public void setOtpDetailsId(String otpDetailsId) {
this.otpDetailsId = otpDetailsId;
}
}
You need to inform somehow to spring to use your message converter.
This should do the work:
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
#Configuration
#EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
public MappingJackson2HttpMessageConverter messageConverter() {
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
jsonConverter.setObjectMapper(objectMapper);
return jsonConverter;
}
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(messageConverter());
}
}
Related
I am working with kafka and spring boot and I need to send JSON object to kafka, the point is that I am able to send an object as JSON configuring KafkaTemplate but just for this object.
package com.bankia.apimanager.config;
import com.bankia.apimanager.model.RequestDTO;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
import org.springframework.kafka.support.serializer.JsonSerializer;
import java.util.HashMap;
import java.util.Map;
#Configuration
public class KafkaConfiguration {
#Value("${spring.kafka.bootstrap-servers}")
private String bootstrapServers;
#Bean
public Map<String, Object> producerConfigs() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
return props;
}
#Bean
public ProducerFactory<String, RequestDTO> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfigs());
}
#Bean
public KafkaTemplate<String, RequestDTO> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
}
package com.bankia.apimanager.controller;
import com.bankia.apimanager.model.RequestDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
#RestController
#RequestMapping("/infrastructure")
public class InfraStructureRequestController {
private final static Logger LOG = LoggerFactory.getLogger( InfraStructureRequestController.class );
private static final String TOPIC = "test";
#Autowired
private KafkaTemplate<String, RequestDTO> sender;
#RequestMapping(value = "/test", method = RequestMethod.GET)
public String postMessage(){
ListenableFuture<SendResult<String, RequestDTO>> future = sender.send(TOPIC, new RequestDTO("Hola","Paco"));
future.addCallback(new ListenableFutureCallback<SendResult<String, RequestDTO>>() {
#Override
public void onSuccess(SendResult<String, RequestDTO> result) {
LOG.info("Sent message with offset=[" + result.getRecordMetadata().offset() + "]");
}
#Override
public void onFailure(Throwable ex) {
LOG.error("Unable to send message due to : " + ex.getMessage());
}
});
return "OK";
}
}
but what about if now I want to send a new DTO object? do I have to declare a new KafkaTemplate<String,NEWOBJECT> and autowire each kafka template declared in configuration for each object? there is another way to be able to just declare one kafkaTemplate in which I can send any type of object and automatically will be serialized in JSON?
I think, you can specify a generic KafkaTemplate<String, Object> and set the producer value serializer to JsonSerializer like this:
#Configuration
public class KafkaConfiguration {
#Value("${spring.kafka.bootstrap-servers}")
private String bootstrapServers;
#Bean
public Map<String, Object> producerConfigs() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
return props;
}
#Bean
public ProducerFactory<String, Object> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfigs());
}
#Bean
public KafkaTemplate<String, Object> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
}
Referring your code:
Value Serializer is correctly defined as JsonSerializer, which will convert objects of any type to JSON.
#Bean
public Map<String, Object> producerConfigs() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
return props;
}
Change <String, RequestDTO> to <String, Object> at every place in KafkaConfig & Controller.
Keep in mind that generics remain until compile time (type erasure)
only.
There are two scenario:
Scenario #1
If you want to use KafkaTemplate to send any type(as mentioned in your question) to kafka, so there is no need to declare your own KafkaTemplate bean because Spring boot did this for you in KafkaAutoConfiguration.
package org.springframework.boot.autoconfigure.kafka;
...
#Configuration(proxyBeanMethods = false)
#ConditionalOnClass(KafkaTemplate.class)
#EnableConfigurationProperties(KafkaProperties.class)
#Import({ KafkaAnnotationDrivenConfiguration.class, KafkaStreamsAnnotationDrivenConfiguration.class })
public class KafkaAutoConfiguration {
private final KafkaProperties properties;
public KafkaAutoConfiguration(KafkaProperties properties) {
this.properties = properties;
}
#Bean
#ConditionalOnMissingBean(KafkaTemplate.class)
public KafkaTemplate<?, ?> kafkaTemplate(ProducerFactory<Object, Object> kafkaProducerFactory,
ProducerListener<Object, Object> kafkaProducerListener,
ObjectProvider<RecordMessageConverter> messageConverter) {
KafkaTemplate<Object, Object> kafkaTemplate = new KafkaTemplate<>(kafkaProducerFactory);
messageConverter.ifUnique(kafkaTemplate::setMessageConverter);
kafkaTemplate.setProducerListener(kafkaProducerListener);
kafkaTemplate.setDefaultTopic(this.properties.getTemplate().getDefaultTopic());
return kafkaTemplate;
}
}
**Some Note**:
This config class has been annotated with #ConditionalOnClass(KafkaTemplate.class) that means: (from spring docs--->) #Conditional that only matches when the specified classes are on the classpath.
kafkaTemplate bean method is annotated with
#ConditionalOnMissingBean(KafkaTemplate.class) that means: (from spring docs ---->) #Conditional that only matches when no beans meeting the specified requirements are already contained in the BeanFactory.
Important! In pure java world, KafkaTemplate<?, ?> is not subtype of for example: KafkaTemplate<String, RequestDTO> so you can't to do this:
KafkaTemplate<?, ?> kf1 = ...;
KafkaTemplate<String, RequestDTO> kf2 = kf1; // Compile time error
because java parameterized types are invariant as mentioned in Effective Java third edition item 31. But is spring world that is ok and will be injected to your own service. You need only to specify your own generic type on your kafkaTemplate properties.
For example:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
#Service
public class KafkaService {
#Autowired
private KafkaTemplate<Integer, String> kafkaTemplate1;
#Autowired
private KafkaTemplate<Integer, RequestDTO> KafkaTemplate2;
}
Scenario #2
If you need to restrict value type of kafka record then you need to specify your own kafka bean something like this:
#Configuration(proxyBeanMethods = false)
#ConditionalOnClass(KafkaTemplate.class)
#EnableConfigurationProperties(CorridorTracingConfiguration.class)
public class CorridorKafkaAutoConfiguration {
#Bean
#ConditionalOnMissingBean(KafkaTemplate.class)
public KafkaTemplate<?, AbstractMessage> kafkaTemplate(ProducerFactory<Object, AbstractMessage> kafkaProducerFactory,
ProducerListener<Object, AbstractMessage> kafkaProducerListener,
ObjectProvider<RecordMessageConverter> messageConverter) {
KafkaTemplate<Object, AbstractMessage> kafkaTemplate = new KafkaTemplate<>(kafkaProducerFactory);
messageConverter.ifUnique(kafkaTemplate::setMessageConverter);
kafkaTemplate.setProducerListener(kafkaProducerListener);
kafkaTemplate.setDefaultTopic(this.properties.getTemplate().getDefaultTopic());
return kafkaTemplate;
}
Now this can be injected only to
KafkaTemplate<String, AbstractMessage> kafkaTemplate, the key type can be anything else instead of String. But you can send any sub type of AbstractMessage to kafka via it.
An example usage:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
#Service
public class KafkaService {
#Autowired
private KafkaTemplate<String, AbstractMessage> kafkaTemplate;
public void makeTrx(TrxRequest trxRequest) {
kafkaTemplate.send("fraud-request", trxRequest.fromAccountNumber(), new FraudRequest(trxRequest));
}
}
#Accessors(chain = true)
#Getter
#Setter
#EqualsAndHashCode(callSuper = true)
#ToString(callSuper = true)
public class FraudRequest extends AbstractMessage {
private float amount;
private String fromAccountNumber;
private String toAccountNumber;
...
}
To restrict the key of kafka message follow the same (above) way
I have an issue serializing a spring managed bean.
I want to return an autowired bean as the response for a restcontroller. I have read several responses, one of which advises using a simpleFilter.(Use SimpleFilter to exclude non required fields.). However I do not think this suggestion is very practical, and moreover, I am sure there is a much more simple and concrete way to solve the problem.
I have a spring managed bean called JobStatus.
#Component
#Scope(value="Prototype")
public class JobStatus{
private Integer job_type;
public Integer getJob_type() {
return job_type;
}
public void setJob_type(Integer job_type) {
this.job_type = job_type;
}
public JobStatus(){
}
}
I have a controller as follows:
#RestController
public class JobController {
#Autowired
JobStatus js;
#RequestMapping(value = "/get_job_status", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public #ResponseBody
JobStatus get_job_status(#RequestBody JobStatusRequest req) {
js.setJobType(req.getJobType);
ObjectMapper mapper = new ObjectMapper();
try {
System.out.println(mapper.writeValueAsString(js));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return js;
}
}
It throws the following exception:
com.fasterxml.jackson.databind.JsonMappingException: No serializer found for class org.springframework.cglib.proxy.NoOp$1 and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) ) (through reference chain: ATM.Job.JobStatus$$EnhancerBySpringCGLIB$$be675215["callbacks"])
I have tried changing the scope of JobStatus to "singleton" and "session" and "request" and it doesn't make any difference. How are we supposed to serialize "proxies"?
You can just tell Jackson: "serialize my class using the very same type as supertype". Since Spring proxies subclass your original class, this seem to work at least on Spring Boot 2.0.4.RELEASE:
#JsonSerialize(as=MyCompontClass.class)
#Component
public class MyCompontClass{
// fields, getters, setters
}
Jackson API docs say:
as
public abstract Class as
Supertype (of declared type, which itself is supertype of runtime
type) to use as type when locating serializer to use.
Create a view class
public class JobStatusView {
public JobStatusView(JobStatus js) {
job_type = js.getJob_type();
}
private Integer job_type;
public Integer getJob_type() {
return job_type;
}
public void setJob_type(Integer job_type) {
this.job_type = job_type;
}
}
Have your controller method return new JobStatusView(js) or create a Factory class or whatever your preferred method for creating instances is.
This has the benefit of separating the data from the view. You can add whichever Jackson annotations on the view class later, if the need arises, without having to pile them into the original bean.
I am not sure if this will work but it worked for me in another context.
You can try using #Configurable on your Jobstatus class(with AspectJ weaving configured) and create a new instance of job status in the controller. Spring would inject the bean whenever JObStatus's new instance is called. You can then serialize the jobstatus object as usual.
If it is acceptable for you to only have fields reachable via public getter method getting serialised, you can configure Jackson to ignore the non-public fields. This results in the proxy fields not being serialised:
add a #Configuration bean somewhere on your class path in a package under where the Application.java class for spring resides:
in there set ObjectMapper properties
objectMapper.setVisibility(PropertyAccessor.ALL,
JsonAutoDetect.Visibility.NONE);
objectMapper.setVisibility(PropertyAccessor.GETTER,
JsonAutoDetect.Visibility.PUBLIC_ONLY);
Here is a complete class:
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import
org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
#Configuration
public class JacksonConfig extends WebMvcConfigurerAdapter {
#Bean
#Primary
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
setup(objectMapper);
return objectMapper;
}
public void setup(ObjectMapper objectMapper) {
objectMapper.setVisibility(PropertyAccessor.ALL,
JsonAutoDetect.Visibility.NONE);
objectMapper.setVisibility(PropertyAccessor.GETTER,
JsonAutoDetect.Visibility.PUBLIC_ONLY);
}
#Override
public void configureMessageConverters(
List<HttpMessageConverter<?>> converters) {
final MappingJackson2HttpMessageConverter converter =
getMappingJackson2HttpMessageConverter();
converters.add(converter);
super.configureMessageConverters(converters);
}
#Bean
#Primary
public MappingJackson2HttpMessageConverter
getMappingJackson2HttpMessageConverter() {
final MappingJackson2HttpMessageConverter converter = new
MappingJackson2HttpMessageConverter();
final ObjectMapper objectMapper = new ObjectMapper();
setup(objectMapper);
converter.setObjectMapper(objectMapper);
converter.setPrettyPrint(true);
return converter;
}
}
When you use Jackson's writerWithView any properties that don't have a #JsonView annotation on them are still serialised. However using #JsonView on a Spring MVC action seems to require #JsonView to be on every property.
If say we have the following model:
public class User {
private String username;
private String emailAddress;
public String getUsername() { return username; }
#JsonView(DetailView.class)
public String getEmailAddress() { return emailAddress; }
}
And DetailView extends BasicView, when I serialise with basic view I'd expect username to be serialised. This is what happens when we use writerWithView:
#RequestMapping(value = "/me", method = GET)
#ResponseBody
public String getMe() throws JsonProcessingException {
User user = getCurrentUser();
return objectMapper.writerWithView(BasicView.class).writeValueAsString(user);
}
However from Spring MVC 4.1 we can instead do the following:
#RequestMapping(value = "/me", method = GET)
#ResponseBody
#JsonView(BasicView.class)
public User getMe() throws JsonProcessingException {
return getCurrentUser();
}
The later causes the response to be {} rather than {username:"David"}. If we add #JsonView(BasicView.class) onto the getUsername() this works as expected.
Obviously we could go with the former or add #JsonView to everything, both of which are more verbose and error prone.
This looks a bit like MapperFeature.DEFAULT_VIEW_INCLUSION has been turned off, but explicitly enabling it doesn't seem to have worked.
Is there anyway to get around this?
MapperFeature.DEFAULT_VIEW_INCLUSION being disabled is indeed the problem, but unfortunately the Spring classes don't give an easy way to configure or replace the ObjectMapper used by the default message converters.
The neatest way I found to work around this was to extend DelegatingWebMvcConfiguration and override configureMessageConverters to populate the default converters and then overwrite the problematic MappingJackson2HttpMessageConverter:
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration;
import com.example.config.serialization.AllEncompassingFormHttpMessageConverterWithCustomObjectMapper;
import java.util.List;
#Configuration
#ComponentScan({/*...*/})
public class MyWebConfig extends DelegatingWebMvcConfiguration {
#Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
//...
return objectMapper;
}
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
super.configureMessageConverters(messageConverters);
if (messageConverters.isEmpty()) {
addDefaultHttpMessageConverters(messageConverters);
}
messageConverters.replaceAll(converter -> {
if (converter instanceof MappingJackson2HttpMessageConverter) {
return new MappingJackson2HttpMessageConverter(objectMapper());
} else if (converter instanceof AllEncompassingFormHttpMessageConverter) {
return new AllEncompassingFormHttpMessageConverterWithCustomObjectMapper(objectMapper());
}
return converter;
});
}
}
For completeness you may also want to substitute the AllEncompassingFormHttpMessageConverter, as above, with your own copy that also allows specification of an ObjectMapper. I've not included the AllEncompassingFormHttpMessageConverterWithCustomObjectMapper class here - it's a trivial copy of AllEncompassingFormHttpMessageConverter that forwards an ObjectMapper constructor parameter to the MappingJackson2HttpMessageConverter that it creates.
Every object with Date format is being serialized as a long.
I've read around that I need to create a custom object mapper
and so I did:
public class CustomObjectMapper extends ObjectMapper {
public CustomObjectMapper() {
super();
configure(Feature.WRITE_DATES_AS_TIMESTAMPS, false);
setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
}
I've also registered that custom mapper as a converter
#Override
protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(converter());
addDefaultHttpMessageConverters(converters);
}
#Bean
MappingJacksonHttpMessageConverter converter() {
MappingJacksonHttpMessageConverter converter = new MappingJacksonHttpMessageConverter();
converter.setObjectMapper(new CustomObjectMapper());
return converter;
}
but still, it doesn't work, and I recieve a long as a date.
Any idea what am I doing wrong?
You'll need to implement your own Dateserializer, just like the following (got it from this tutorial, so props to Loiane, not me ;-) ):
package ....util.json;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.map.JsonSerializer;
import org.codehaus.jackson.map.SerializerProvider;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
#Component
public class JsonDateSerializer extends JsonSerializer<Date>{
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm "); // change according to your needs
#Override
public void serialize(Date date, JsonGenerator gen, SerializerProvider provider)
throws IOException {
String formattedDate = dateFormat.format(date);
gen.writeString(formattedDate);
}
}
then you could just add the following annotation to your Date-Objects and it will persist fine:
#JsonSerialize(using = JsonDateSerializer.class)
public Date getCreated() {
return created;
}
At least it works with spring 3.2.4 and jackson 1.9.13 here.
edit: Think about using FastDateFormat instead of SimpleDateFormat, for it's the threadsafe-alternative (as mentioned in the comments of Loianes article)
Try adding 0 as index in #add()
#Configuration
#ComponentScan()
#EnableWebMvc
#PropertySource("classpath:/web.properties")
public class WebConfig extends WebMvcConfigurerAdapter
{
#Override
public void configureMessageConverters(final List<HttpMessageConverter<?>> converters)
{
converters.add(0, jsonConverter());
}
#Bean
public MappingJacksonHttpMessageConverter jsonConverter()
{
final MappingJacksonHttpMessageConverter converter = new MappingJacksonHttpMessageConverter();
converter.setObjectMapper(new CustomObjectMapper());
return converter;
}
}
It worked for me.
Does anyone know if there is a Spring MVC mapping view for Gson? I'm looking for something similar to org.springframework.web.servlet.view.json.MappingJacksonJsonView.
Ideally it would take my ModelMap and render it as JSON, respecting my renderedAttributes set in the ContentNegotiatingViewResolver declaration
We plan to use Gson extensively in the application as it seems safer and better than Jackson. That said, we're getting hung up by the need to have two different JSON libraries in order to do native JSON views.
Thanks in advance!
[cross-posted to Spring forums]
aweigold got me most of the way there, but to concretely outline a solution for Spring 3.1 Java based configuration, here's what I did.
Grab GsonHttpMessageConverter.java from the spring-android-rest-template project.
Register your GsonHttpMessageConverter with the message converters in your MVC config.
#EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new GsonHttpMessageConverter());
}
}
The Spring docs outline this process, but aren't crystal clear. In order to get this to work properly, I had to extend WebMvcConfigurerAdapter, and then override configureMesageConverters. After doing this, you should be able to do the following in your controller method:
#Controller
public class AppController {
#RequestMapping(value = "messages", produces = MediaType.APPLICATION_JSON_VALUE)
public List<Message> getMessages() {
// .. Get list of messages
return messages;
}
}
And voila! JSON output.
I would recommend to extend AbstractView just like the MappingJacksonJsonView does.
Personally, for JSON, I prefer to use #Responsebody, and just return the object rather than a model and view, this makes it easier to test. If you would like to use GSON for that, just create a custom HttpMessageConverter like this:
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import com.vitalimages.string.StringUtils;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.stereotype.Component;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.sql.Timestamp;
#Component
public class GSONHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private GsonBuilder gsonBuilder = new GsonBuilder()
.excludeFieldsWithoutExposeAnnotation()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
.registerTypeAdapter(Timestamp.class, new GSONTimestampConverter());
public GSONHttpMessageConverter() {
super(new MediaType("application", "json", DEFAULT_CHARSET));
}
#Override
protected boolean supports(Class<?> clazz) {
// should not be called, since we override canRead/Write instead
throw new UnsupportedOperationException();
}
#Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return MediaType.APPLICATION_JSON.isCompatibleWith(mediaType);
}
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return MediaType.APPLICATION_JSON.isCompatibleWith(mediaType);
}
public void registerTypeAdapter(Type type, Object serializer) {
gsonBuilder.registerTypeAdapter(type, serializer);
}
#Override
protected Object readInternal(Class<? extends Object> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
try {
Gson gson = gsonBuilder.create();
return gson.fromJson(StringUtils.convertStreamToString(inputMessage.getBody()), clazz);
} catch (JsonParseException e) {
throw new HttpMessageNotReadableException("Could not read JSON: " + e.getMessage(), e);
}
}
#Override
protected void writeInternal(Object o, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
Type genericType = TypeToken.get(o.getClass()).getType();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputMessage.getBody(), DEFAULT_CHARSET));
try {
// See http://code.google.com/p/google-gson/issues/detail?id=199 for details on SQLTimestamp conversion
Gson gson = gsonBuilder.create();
writer.append(gson.toJson(o, genericType));
} finally {
writer.flush();
writer.close();
}
}
}
And then add it to your converter list in your handler adapter like this:
#Bean
public HandlerAdapter handlerAdapter() {
final AnnotationMethodHandlerAdapter handlerAdapter = new AnnotationMethodHandlerAdapter();
handlerAdapter.setAlwaysUseFullPath(true);
List<HttpMessageConverter<?>> converterList = new ArrayList<HttpMessageConverter<?>>();
converterList.addAll(Arrays.asList(handlerAdapter.getMessageConverters()));
converterList.add(jibxHttpMessageConverter);
converterList.add(gsonHttpMessageConverter);
handlerAdapter.setMessageConverters(converterList.toArray(new HttpMessageConverter<?>[converterList.size()]));
return handlerAdapter;
}