Custom object mapper not taking effect in springboot - spring-boot

I've a springboot application. I'm trying to replace null values with a custom string ("NA"). So, I've configured an object mapper with a custom serializer provider that in turn has a custom null value serializer. You can check the code below.
#Configuration
public class JacksonConfig {
#Bean
public ObjectMapper jacksonObjectMapper() {
return new CustomObjectMapper();
}
#Bean
public SerializationConfig serializationConfig() {
return jacksonObjectMapper().getSerializationConfig();
}
}
class CustomObjectMapper extends ObjectMapper {
CustomObjectMapper() {
super();
DefaultSerializerProvider.Impl serializerProvider = new DefaultSerializerProvider.Impl();
serializerProvider.setNullValueSerializer(new JsonSerializer<Object>() {
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString("NA");
}
});
this.setSerializerProvider(serializerProvider);
}
}
Now the problem is, even these beans are getting created, the custom null value serializer is still not taking effect. In the response, I'm still seeing lot of null values. Is there anything else I'm missing here?

Related

LocalDateTime not serializing based on given serializer registered with JavaTimeModule

I'm facing an issue where Spring boot (v2.6.13) is not parsing LocalDateTime based on a registered serializer, the response of LocalDateTime of RestController is always an array of integers.
#Bean
public Module javaTimeModule() {
JavaTimeModule module = new JavaTimeModule();
module.addSerializer(new CustomLocalDateTimeSerializer());
return module;
}
class CustomLocalDateTimeSerializer extends StdSerializer<LocalDateTime> {
private static DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd");
protected CustomLocalDateTimeSerializer() {
super(LocalDateTime.class);
}
#Override
public void serialize(
LocalDateTime localDateTime,
JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(localDateTime.format(formatter));
}
}
Notes:
Injecting object mapper and serialize the object returns the correct format.
I've defined an object mapper annotated with #Primary, but still facing the same issue.
I want to configure everything globally - don't wanna use #JsonSerialize on each attribute-
it seems like Spring is using a different object mapper for serializing a method returned object.
I've found the issue and resolved it.
I have a WebMvcConfigurationSupport configuration which was overriding the configured object mapper.
I've solved it using following code
private final ObjectMapper objectMapper;
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters)
{
var converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(objectMapper);
converters.add(converter);
super.configureMessageConverters(converters);
}

InvalidFormatException for Date - fixing without using JsonFormat or modifying original class

Introduction
We are using a custom starter hosted on a nexus repository, that contains spring-cloud-feign clients that make requests to microservices.
One of the microservices returns the dates as "dd-MM-yyyy HH:mm:ssZ" and this works in most of our applications. However, we have one application that is throwing the following error:
Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2019-10-16 14:23:17": not a valid representation (error: Failed to parse Date value '2019-10-16 14:23:17': Unparseable date: "2019-10-16 14:23:1
7")
Current work-around
My Current work-around, as I don't want to pollute the starter, is to extend the class and create a local feign-client and local pojo with the proper JsonFormat:
public class DocumentMetaDataFix extends DocumentMetaData {
#JsonFormat(
shape = Shape.STRING,
pattern = "yyyy-MM-dd HH:mm:ss"
)
private Date creationDate;
#JsonFormat(
shape = Shape.STRING,
pattern = "yyyy-MM-dd HH:mm:ss"
)
Failed Fixes
I have tried the following in my configuration class, in order to try affecting the de-serialization from another path. However, the DocumentMetaDataSerializer is never called. The ObjectMapper bean IS called.
#Configuration
#EnableSpringDataWebSupport
#RequiredArgsConstructor
public class MyConfig extends WebMvcConfigurerAdapter {
#Bean
public Jackson2ObjectMapperBuilderCustomizer addCustomBigDecimalDeserialization() {
return new Jackson2ObjectMapperBuilderCustomizer() {
#Override
public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
jacksonObjectMapperBuilder.deserializerByType(DocumentMetaData.class, new DocumentMetaDataDeserializer());
}
};
}
#Primary
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
mapper.setDateFormat(new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"));
//mapper.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, true);
return mapper;
}
#Bean
public Module dynamoDemoEntityDeserializer() {
SimpleModule module = new SimpleModule();
module.addDeserializer(DocumentMetaData.class, new DocumentMetaDataDeserializer());
return module;
}
public static class DocumentMetaDataDeserializer extends JsonDeserializer<DocumentMetaData> {
#Override
public DocumentMetaData deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
// return DynamoDemoEntity instance;
JsonNode node = jp.getCodec().readTree(jp);
return null;
}
public DocumentMetaData deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer t) throws IOException {
JsonNode node = jp.getCodec().readTree(jp);
return null;
}
}
Full Stacktrace
Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2019-10-16 14:23:17": not a valid representation (error: Failed to parse Date value '2019-10-16 14:23:17': Unparseable date: "2019-10-16 14:23:1
7")
at [Source: (ByteArrayInputStream); line: 1, column: 580] (through reference chain: eu.europa.ec.nova.documentstore.DocumentMetaData["creationDate"])
at com.fasterxml.jackson.databind.exc.InvalidFormatException.from(InvalidFormatException.java:67)
at com.fasterxml.jackson.databind.DeserializationContext.weirdStringException(DeserializationContext.java:1548)
at com.fasterxml.jackson.databind.DeserializationContext.handleWeirdStringValue(DeserializationContext.java:910)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer._parseDate(StdDeserializer.java:524)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer._parseDate(StdDeserializer.java:467)
at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateBasedDeserializer._parseDate(DateDeserializers.java:195)
at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateDeserializer.deserialize(DateDeserializers.java:285)
at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateDeserializer.deserialize(DateDeserializers.java:268)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:127)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:288)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4013)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3084)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:237)
... 70 common frames omitted
So, any ideas?
I have searched through the project for references to Jackson in case there is anything else in my project causing this.
I am will try to go inside the ObjectMapper and try to debug the current parameters/fields of the configuration at ObjectMapper.java:3084 from the stacktace:
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3084)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:237)
... 67 common frames omitted
Update
I added a breakpoint in the objectmapper constructor, and am seeing that it is being initialized from more than one location. This led me to suspect that spring-boot is not using my ObjectMapper. Instead it is using an internal spring one that is called from MappingJackson2HttpMessageConverter .
<init>:480, ObjectMapper
build:606, Jackson2ObjectMapperBuilder
<init>:59, MappingJackson2HttpMessageConverter
<init>:74, AllEncompassingFormHttpMessageConverter
I will therefore try to over-ride this internal spring one, based on results I found from: How to customise the Jackson JSON mapper implicitly used by Spring Boot?
However this also failed.
References
Is it possible to configure Jackson custom deserializers at class level for different data types?
https://docs.spring.io/spring-boot/docs/current/reference/html/howto-spring-mvc.html#howto-customize-the-jackson-objectmapper
https://www.baeldung.com/jackson-deserialization
very useful: https://mostafa-asg.github.io/post/customize-json-xml-spring-mvc-output/
How to customise Jackson in Spring Boot 1.4
Update - final list of tries
It still fails with an error.
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.serializationInclusion(JsonInclude.Include.NON_NULL);
builder.propertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
builder.serializationInclusion(Include.NON_EMPTY);
builder.indentOutput(true).dateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()));
//converters.add(cmsaMessageConverter());
converters.add(new StringHttpMessageConverter());
converters.add(new FormHttpMessageConverter());
converters.add(new MappingJackson2HttpMessageConverter());
}
#Bean
public Jackson2ObjectMapperBuilderCustomizer addCustomBigDecimalDeserialization() {
return new Jackson2ObjectMapperBuilderCustomizer() {
#Override
public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
jacksonObjectMapperBuilder.deserializerByType(DocumentMetaData.class, new DocumentMetaDataDeserializer());
}
};
}
#Primary
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
mapper.setDateFormat(new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"));
//mapper.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, true);
return mapper;
}
#Bean
public Module dynamoDemoEntityDeserializer() {
SimpleModule module = new SimpleModule();
module.addDeserializer(DocumentMetaData.class, new DocumentMetaDataDeserializer());
return module;
}
public static class DocumentMetaDataDeserializer extends JsonDeserializer<DocumentMetaData> {
#Override
public DocumentMetaData deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
// return DynamoDemoEntity instance;
JsonNode node = jp.getCodec().readTree(jp);
return null;
}
public DocumentMetaData deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer t) throws IOException {
JsonNode node = jp.getCodec().readTree(jp);
return null;
}
}
It still fails with an error.
Try using LocalDateTime,
this is what I'm doing and working for me
#JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime date;

spring test rest template : Could not extract response: no suitable HttpMessageConverter found for response type

I Get this exception
org.springframework.web.client.RestClientException: Could not extract
response: no suitable HttpMessageConverter found for response type [AnalyticsResponse] and content type [application/json;charset=UTF-8]
in my junit test cases only(Rest endpoints work fine) if my DTO contains a map with a user defined class as a key
#Data
public class AnalyticsResponse {
private List<Committer> commitersList; //OK
private Map<Committer , Long> comittersCommitsMap; // Problem
private Map<Date, List<CommitItem>> commitItemsTimeLineMap; //OK
}
If comittersCommitsMap field is removed, every thing goes fine
my test case code snippet:
ResponseEntity<AnalyticsResponse> analyticsResponse = testRestTemplate.getForEntity(ANALYSIS_CONTROLLER_BASE_URL+"analytics?repo-full-name=" + searchResponse.getBody().get(0).getFull_name() ,
AnalyticsResponse.class);
---update : Committer class
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Committer {
private String name;
private String email;
}
By default, the ObjectMapper cannot determine serialization and deserialization for a Map in which a key is not a String, you have to provide your custom implementation.
Implement KeyDeserializer and JsonSerializer for Committer and Date(as key for Map)
Configure ObjectMapper - register module with KeyDeserializer and KeySerializer(JsonSerializer)
#SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
// KeyDeserializer for Committer (simple without 'null' check)
#RequiredArgsConstructor
public static class CommitterKeyDeserializer extends KeyDeserializer {
private final ObjectMapper mapper;
#Override
public Object deserializeKey(final String key,
final DeserializationContext ctxt)
throws IOException, JsonProcessingException {
return mapper.readValue(key, Committer.class);
}
}
// KeyDeserializer for Date (simple without 'null' check)
#RequiredArgsConstructor
public static class DateKeyDeserializer extends KeyDeserializer {
private final ObjectMapper mapper;
#Override
public Object deserializeKey(final String key,
final DeserializationContext ctxt)
throws IOException, JsonProcessingException {
return mapper.readValue(key, Date.class);
}
}
// JsonSerializer for Committer (simple without 'null' check)
#RequiredArgsConstructor
public static class CommitterJsonSerializer extends JsonSerializer<Committer> {
private final ObjectMapper mapper;
#Override
public void serialize(Committer committer,
JsonGenerator jgen,
SerializerProvider provider)
throws IOException, JsonProcessingException {
jgen.writeFieldName(mapper.writeValueAsString(committer));
}
}
// JsonSerializer for Date (simple without 'null' check)
public static class DateJsonSerializer extends JsonSerializer<Date> {
#Override
public void serialize(Date date,
JsonGenerator jgen,
SerializerProvider provider)
throws IOException, JsonProcessingException {
jgen.writeFieldName(String.valueOf(date.getTime()));
}
}
// ObjectMapper configuration
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
// register module with custom serializers and deserializers
mapper.registerModule(new SimpleModule()
.addKeyDeserializer(
Committer.class,
new CommitterKeyDeserializer(mapper))
.addKeyDeserializer(
Date.class,
new DateKeyDeserializer(mapper))
.addKeySerializer(
Committer.class,
new CommitterJsonSerializer(mapper))
.addKeySerializer(
Date.class,
new DateJsonSerializer()));
return mapper;
}
// RestTemplate configuration
#Bean
public RestTemplate restTemplate(List<HttpMessageConverter<?>> converters) {
RestTemplate restTemplate = new RestTemplate();
// add spring's predefined converters
restTemplate.setMessageConverters(converters);
return restTemplate;
}
}
Note that in this simple implementation the key Committer in comittersCommitsMap represented as a String in JSON response (RestTemplate with this implementation works as well):
{
"commitersList": [
{
"name": "name",
"email": "email"
}
],
"comittersCommitsMap": {
"{\"name\":\"name\",\"email\":\"email\"}": 1
},
"commitItemsTimeLineMap": {
"1570929503854": [
{
"data": "data"
}
]
}
}

json serializer with spring boot

I have BigDecimalSerializer
public class BigDecimalSerializer extends JsonSerializer<BigDecimal> {
#Override
public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
gen.writeString(value.setScale(6, BigDecimal.ROUND_HALF_UP).toString());
}
}
and then
#JsonSerialize(using = BigDecimalSerializer.class)
private BigDecimal foo;
is there any way that instead of doing annotate in each member variable, I tell the spring boot at once that apply to all project ?
Try configuring the ObjectMapper by adding a custom module. In case you're using spring-data-rest this can look like this:
#Configuration
public static class ObjectMapperConfigurer extends RepositoryRestConfigurerAdapter {
#Override
public void configureJacksonObjectMapper(final ObjectMapper objectMapper) {
SimpleModule myModule = new SimpleModule();
myModule.addSerializer(BigDecimal.class, BigDecimalSerializer.class);
objectMapper.registerModule(myModule));
}
}
Otherwise simply provide your own ObjectMapper bean and configure it on creation.

Rest Custom HTTP Message Converter Spring Boot 1.2.3

I want to create a custom of HttpMessageConverter using Rest, Json, Spring Boot 1.2.3 and Spring 4, However my custom HTTPMessageConverter its' never called.
I have preformed the following steps :
1: Created a class that extends AbstractHttpMessageConverter
#Component
public class ProductConverter extends AbstractHttpMessageConverter<Employee> {
public ProductConverter() {
super(new MediaType("application", "json", Charset.forName("UTF-8")));
System.out.println("Created ");
}
#Override
protected boolean supports(Class<?> clazz) {
return false;
}
#Override
protected Employee readInternal(Class<? extends Employee> clazz,
HttpInputMessage inputMessage) throws IOException,
HttpMessageNotReadableException {
InputStream inputStream = inputMessage.getBody();
System.out.println("Test******");
return null;
}
#Override
protected void writeInternal(Employee t,
HttpOutputMessage outputMessage) throws IOException,
HttpMessageNotWritableException {
// TODO Auto-generated method stu
}
}
2: I create a configuration class to register HTTPMessageConverters
#Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter{
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
System.out.println("Configure Message Converters");
converters.add(new ProductConverter());
super.configureMessageConverters(converters);
//super.extendMessageConverters(converters);
}
}
3: The rest class method
#RequestMapping(value="/{categoryId}" ,method=RequestMethod.POST, consumes="application/json")
#PreAuthorize("permitAll")
public ResponseEntity<ProductEntity> saveProduct(#RequestBody Employee employee , #PathVariable Long categoryId) {
logger.log(Level.INFO, "Category Id: {0}" , categoryId);
ResponseEntity<ProductEntity> responseEntity =
new ResponseEntity<ProductEntity>(HttpStatus.OK);
return responseEntity;
}
My Custom HTTPMessageCoverter it's created but is never called ? Is there a configuration or step I'm missing ? any input or advice is appreciated.
After overriding the (AbstractHttpMessageConverter) class methods, I found out there's two annotations for achieving polymorphism #JsonTypeInfo and #JsonSubTypes. For anyone who wants achieve polymorphism can use these two annotations.
I believe you want to configure these message converters using the configureMessageConverters method in a configuration class that extends WebMvcConfigurerAdapter. I've done this myself with a converter for CSV content. I've included that code below. This link shows an example as well. This link may also be helpful. It seems like with Spring configuration it is not always clear on the best place to configure things. :) Let me know if this helps.
#Configuration
public class ApplicationWebConfiguration extends WebMvcConfigurerAdapter {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
super.configureMessageConverters(converters);
converters.add(new CsvMessageConverter());
}
}
You will also need top modify your supports() method to return true for classes supported by the converter. See the Spring doc for AbstractHttpMessageConverter supports method.

Resources