Sunday, 10 March 2019

Spring Boot 2 and custom JsonSerializer

I was migrating recently to the new Spring Boot version 2. And most annoying thing was that PageImpl (component used for pagination)  changed format. Also I had some issues with Mockito 2, but all of them could be solved read here.


Well I found this article and it all looked easy despite the fact that Spring did not want to use my JsonSerializer. Here how it looks:

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.jackson.JsonComponent;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Sort;
import java.io.IOException;

@JsonComponent
public class PageImplJacksonSerializer extends JsonSerializer {

    @Override
    public void serialize(PageImpl page, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeStartObject();
        jsonGenerator.writeObjectField("content", page.getContent());
        jsonGenerator.writeBooleanField("first", page.isFirst());
        jsonGenerator.writeBooleanField("last", page.isLast());
        jsonGenerator.writeNumberField("totalPages", page.getTotalPages());
        jsonGenerator.writeNumberField("totalElements", page.getTotalElements());
        jsonGenerator.writeNumberField("numberOfElements", page.getNumberOfElements());
        jsonGenerator.writeNumberField("size", page.getSize());
        jsonGenerator.writeNumberField("number", page.getNumber());
        Sort sort = page.getSort();
        jsonGenerator.writeArrayFieldStart("sort");
        for (Sort.Order order : sort) {
            jsonGenerator.writeStartObject();
            jsonGenerator.writeStringField("property", order.getProperty());
            jsonGenerator.writeStringField("direction", order.getDirection().name());
            jsonGenerator.writeBooleanField("ignoreCase", order.isIgnoreCase());
            jsonGenerator.writeStringField("nullHandling", order.getNullHandling().name());
            jsonGenerator.writeEndObject();
        }
        jsonGenerator.writeEndArray();
        jsonGenerator.writeEndObject();
    }
}
Next I read Spring docs of how can I customize Serializer here and here They suggested to create jacksonBuilder with your specific module or register JsonComponentModule that will find your serializer that will be used by spring during mapper creation. Unfortunately none of this worked for me:


    @Bean
    public Module jsonComponentModule() {
        return new JsonComponentModule();
    }

And this also didn't work:

    @Bean
    @Primary
    public Jackson2ObjectMapperBuilder jacksonBuilder() {
        JsonComponentModule module  = new JsonComponentModule();
        module.addSerializer(PageImpl.class, new PageImplJacksonSerializer());
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        builder.modulesToInstall(module);
        return builder;
    }
I continued with my last idea, suggested by docs is configuring HttpMessageConverter. And here I figured out that my config extends WebMvcConfigurationSupport to enable MVC and after debugging spring context creation. I found that Spring registers default converters with method addDefaultHttpMessageConverters and following line of code and for some reason it does not register serializer here:

messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));


So here is working solution for me, with configuring default converter created by Spring:

@Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        MappingJackson2HttpMessageConverter converter = (MappingJackson2HttpMessageConverter)
                converters.stream()
                        .filter(c -> c instanceof MappingJackson2HttpMessageConverter)
                        .findFirst().get();
        Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
        JsonComponentModule module  = new JsonComponentModule();
        module.addSerializer(PageImpl.class, new PageImplJacksonSerializer());
        ObjectMapper objectMapper = builder.modules(module).build();
        converter.setObjectMapper(objectMapper);
    }

2 comments:

  1. Hi,

    Actually there is a simple way that configure PageImplJacksonSerializer's path for component scanning. JsonComponentModule(org.springframework.boot.jackson) registers json beans which annotated with @JsonComponent.

    @Configuration
    @ComponentScan("com.progrnotes.serializer")
    public class SerializerConfig {

    }

    ReplyDelete
    Replies
    1. unfortunately it didn't work for me, spring find it but do not use when i return json from controller

      Delete