4

Here's my object mapper configuration. I provide my own object mapper so that Spring boot's 2.0's included object mapper is overridden. Despite that I also included the JsonComponentModule() so that I could use the @JsonComponent annotation to pickup custom serializers.

public class ObjectMapperConfigurer {
    public static ObjectMapper configureObjectMapper(ObjectMapper objectMapper) {
        return objectMapper.registerModules(
                // First three modules can be found here. https://github.com/FasterXML/jackson-modules-java8
                new Jdk8Module(), // support for other new Java 8 datatypes outside of date/time: most notably Optional, OptionalLong, OptionalDouble
                new JavaTimeModule(), // support for Java 8 date/time types (specified in JSR-310 specification)
                new ParameterNamesModule(), // Support for detecting constructor and factory method ("creator") parameters without having to use @JsonProperty annotation
                // These two modules are provided by spring
                new JsonComponentModule(), // Enables https://docs.spring.io/spring-boot/docs/2.1.0.RELEASE/reference/htmlsingle/#boot-features-json-components
                new GeoModule(), // Enables marshalling of GeoResult<T>, GeoResults<T>, and GeoPage<T>
                new Hibernate5Module().enable(Hibernate5Module.Feature.FORCE_LAZY_LOADING) // Allows jackson to gracefully handle Hibernate lazy loading,
        )
                .setSerializationInclusion(JsonInclude.Include.NON_NULL)
                // Turn on/off some features. https://github.com/FasterXML/jackson-databind/wiki/JacksonFeatures
                .enable(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS)
                .enable(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY)
                .enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS)
                .enable(SerializationFeature.INDENT_OUTPUT)
                .enable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS)
                .disable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)
                .disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS)
                .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
                .disable(MapperFeature.DEFAULT_VIEW_INCLUSION)
                .setFilterProvider(new SimpleFilterProvider().addFilter(FilteringAnnotationInspector.DEFAULT_FILTER, new DepthFilter()))
                .setAnnotationIntrospector(new FilteringAnnotationInspector());
    }

    public static ObjectMapper configureObjectMapper() {
        return configureObjectMapper(new ObjectMapper());
    }

}

Here's my custom serializer. It's okay if the implementation of the serialization is wrong, my core problem is that it's not getting invoked.

@JsonComponent
public class PageJsonSerializer extends JsonSerializer<Page> {

    @Override
    public void serialize(Page page, JsonGenerator jsonGen, SerializerProvider serializerProvider) throws IOException {
        System.out.println("serializing using pagejsonserializer");
        ObjectMapper om = new ObjectMapper()
                .disable(MapperFeature.DEFAULT_VIEW_INCLUSION)
                .setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
        jsonGen.writeStartObject();
        jsonGen.writeFieldName("size");
        jsonGen.writeNumber(page.getSize());
        jsonGen.writeFieldName("number");
        jsonGen.writeNumber(page.getNumber());
        jsonGen.writeFieldName("totalElements");
        jsonGen.writeNumber(page.getTotalElements());
        jsonGen.writeFieldName("last");
        jsonGen.writeBoolean(page.isLast());
        jsonGen.writeFieldName("totalPages");
        jsonGen.writeNumber(page.getTotalPages());
        jsonGen.writeObjectField("sort", page.getSort());
        jsonGen.writeFieldName("first");
        jsonGen.writeBoolean(page.isFirst());
        jsonGen.writeFieldName("numberOfElements");
        jsonGen.writeNumber(page.getNumberOfElements());
        jsonGen.writeFieldName("content");
        jsonGen.writeRawValue(om.writerWithView(serializerProvider.getActiveView()).writeValueAsString(page.getContent()));
        jsonGen.writeEndObject();
    }
}

Here's the controller returning the Page(d) object.

@GetMapping(JobController.uri.path)
@PreAuthorize("hasAuthority('" + SpringSecurityConfig.Authority.LIST_JOBS + "')")
public Page<Job> listJobs(Pageable pageable) {
  return jobRepository.findAllByCandidates(currentUserHolder.getUser(), pageable);
}

I expect the above to lead to the calling the PageJsonSerializer class. I can see that class getting registered successfully with the JsonComponentModule(). I've also tried many different variations of the class declaration. Like the following but none of them have had any effect.

public class PageJsonSerializer extends JsonSerializer<Page<?>> {
public class PageJsonSerializer extends JsonSerializer<PageImpl> {

I'm not sure what to check next. I'm also struggling to see where Jackson lines up the object it's about to serialize with a serializer while debugging the return trip from the JobController.

Finally, and I don't thinks this is an issue, I'm using some RestControllerAdvice extending the AbstractMappingJacksonResponseBodyAdvice which picks the jsonView I want to use based on the currentUser's role.

EDIT: I just commented out the controller advice, and it's still not calling my custom deserializer.

@RestControllerAdvice
@Slf4j
class SecurityJsonViewControllerAdvice extends AbstractMappingJacksonResponseBodyAdvice {


    @Override
    protected void beforeBodyWriteInternal(
            @NotNull MappingJacksonValue bodyContainer,
            @NotNull MediaType contentType,
            @NotNull MethodParameter returnType,
            @NotNull ServerHttpRequest request,
            @NotNull ServerHttpResponse response) {
        if (SecurityContextHolder.getContext().getAuthentication() != null && SecurityContextHolder.getContext().getAuthentication().getAuthorities() != null) {
            Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
            Class<?> viewClass = User.Role.jsonViewFrom(authorities);
            if (true || log.isDebugEnabled()) {
                log.debug("Deserializing using view {}", viewClass.getSimpleName());
            }
            bodyContainer.setSerializationView(viewClass);
        }
    }
}
Erwin Bolwidt
  • 30,799
  • 15
  • 56
  • 79
Jazzepi
  • 5,259
  • 11
  • 55
  • 81
  • What happens when you do not try to create your own ObjectMapper but instead lets springs Jackson auto-configuration do that? – Erwin Bolwidt Mar 31 '19 at 23:14
  • @ErwinBolwidt leaving in Spring boot's objectmapper is not an option for me. I want full control over it. See my answer, AFAIK the problem laid with spring boot's JsonComponentModule not working properly for whatever reason. – Jazzepi Mar 31 '19 at 23:51
  • If your `ObjectMapperConfigurer` doesn't have a `@Configuration` annotation, how does it even get loaded? If it works with the spring Jackson auto-configuration, then the problem is in the way that you configure Jackson, that's why I'm asking. You can fine-tune the autoconfiguration with `Jackson2ObjectMapperBuilderCustomizer` implementations,. – Erwin Bolwidt Apr 01 '19 at 01:01
  • @ErwinBolwidt The ObjectMapperConfigurer is just broken out into an object so that it's easy to share between test code and production code (I want to use the same object mapper in my test scenarios as I do in my prod ones). The configurer builds up the ObjectMapper that's returned from an @ Bean annotated method in an @ Configuration annotated class. – Jazzepi Apr 01 '19 at 02:04

2 Answers2

2

In my case I had to manually configure MessageConverters. I added this into implementation of WebMvcConfigurer:

  @Override
  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
      Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
      builder.serializerByType(PageImpl.class, new PageJsonSerializer());
      converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
  }
gawi
  • 2,843
  • 4
  • 29
  • 44
1

I was able to fix my problem by doing two things.

@Component
public class PageJsonSerializer extends StdSerializer<Page> {

extending StdSerializer instead of JsonSerializer I actually don't know if this was part of the solution.

I think the real help came from registering the serializer by hand instead of relying on @JsonComponent.

So my ObjectMapperConfigurer looks like this now.

public class ObjectMapperConfigurer {
    public static ObjectMapper configureObjectMapper(ObjectMapper objectMapper) {
        return objectMapper.registerModules(
                // First three modules can be found here. https://github.com/FasterXML/jackson-modules-java8
                new Jdk8Module(), // support for other new Java 8 datatypes outside of date/time: most notably Optional, OptionalLong, OptionalDouble
                new JavaTimeModule(), // support for Java 8 date/time types (specified in JSR-310 specification)
                new ParameterNamesModule(), // Support for detecting constructor and factory method ("creator") parameters without having to use @JsonProperty annotation
                // Manually registering my serializer. 
                new SimpleModule().addSerializer(Page.class, pageJsonSerializer),
... all the same
}

I also removed the JsonComponentModule from my ObjectMapper since it seems to be broken.

Jazzepi
  • 5,259
  • 11
  • 55
  • 81
  • addSerializer method accepts only JsonSerializer. Are you sure you put all these changes at the same time? – gawi Sep 20 '19 at 15:17