javaspringspring-mvcjackson-databind

Custom StringDeserializer in a Spring Web MVC Project


I'm working on a Spring MVC project and I need to globally override the default StringDeserializer provided by Jackson to sanitize incoming JSON strings. My goal is to apply this sanitization to all String fields in my DTOs without having to annotate each fiels individually (more that 200 DTOs to cover).

I have created a custom StringDeserializer and added it in a Spring Configuration, but it is never called.

The custom deserializer:

import org.jsoup.Jsoup;
import org.jsoup.safety.Safelist;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StringDeserializer;

public class SanitizeDeserializer extends StringDeserializer {

  @Override
  public String deserialize(
      final JsonParser aParser,
      final DeserializationContext aContext) throws IOException {
    // this code is never executed
    return Jsoup.clean(super.deserialize(aParser, aContext), Safelist.none());
  }    
}

The Spring configuration to register the Deserializer:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;

@Configuration
public class SanitizeConfiguration {
    
  @Bean
  public Module addSanitizeStringDeserializer() {
    // this code is well executed when running the app
    final SimpleModule module = new SimpleModule();
    module.addDeserializer(String.class, new SanitizeDeserializer());
    return module;
  }
}

REST controller sample:

 @PutMapping("/document/{id}")
    public ResponseEntity<String> save(@RequestBody DocumentDTO document) {
        return ResponseEntity.ok(document.getId());
    }

@Getters
@Setters
public class DocumentDTO {
  // I want these String fields to be sanitize before the setter is called when deserializing the JSON
  private String id;
  private String title;
  private String content;
}

I put a breakpoint in the deserialize method, but it is never called, it breaks in the Jackson StringDeserializer method.

Am I doing something wrong, or am I missing a configuration?

Or maybe it is not feasible to customize serializer on an already defined type?

Edit 1:

I tried configuring MessageConverters, but it override the default conf (WebMvcConfigurationSupport.addDefaultHttpMessageConverters), and while all String fields are well deserialized by custom deserializer, the XML content conversion is not working as before.

  @Override
  public void configureMessageConverters(
      final List<HttpMessageConverter<?>> aConverters) {
    final Jackson2ObjectMapperBuilder iBuilder = new Jackson2ObjectMapperBuilder().deserializerByType(String.class,
        new SanitizeDeserializer());
    aConverters.add(new MappingJackson2HttpMessageConverter(iBuilder.build()));
    // if i remove the following line, the XML response are not handle (500 error)
    // but with this line, XML reponse are XML escaped and become invalid
    aConverters.add(new MappingJackson2XmlHttpMessageConverter(new Jackson2ObjectMapperBuilder().xml().build()));
  }

Then I tried what suggested in comment, to add a Jackson2ObjectMapperBuilder Bean, but it is still not used:

  @Bean
  public Jackson2ObjectMapperBuilder configureJackson2ObjectMapperBuilder() {
    final Jackson2ObjectMapperBuilder iBuilder = new Jackson2ObjectMapperBuilder();
    final SimpleModule iSanitizeModule = new SimpleModule("sanitize-module");
    iSanitizeModule.addDeserializer(String.class, new SanitizeDeserializer());
    iBuilder.modules(iSanitizeModule);
    return iBuilder;
  }

  @Bean
  public MappingJackson2HttpMessageConverter configureMappingJackson2HttpMessageConverter() {
    final MappingJackson2HttpMessageConverter iConverter = new MappingJackson2HttpMessageConverter();
    iConverter.setObjectMapper(configureJackson2ObjectMapperBuilder().build());
    return iConverter;
  }

Solution

  • Here is the working solution, that keeps default Spring MessageConverters, and add a customized deserializer in the Jackson JSON converter.

    The code for the custom Deserializer

    public class SanitizeDeserializer extends StringDeserializer {
    
      @Override
      public String deserialize(
          final JsonParser aParser,
          final DeserializationContext aContext) throws IOException {
        return Jsoup.clean(super.deserialize(aParser, aContext), Safelist.none());
      }    
    }
    

    And then, in @EnableWebMvc class extending WebMvcConfigurer, override the extendMessageConverters to replace the default Jackson JSON MessageConverter by your custom one:

    @Override
    public void extendMessageConverters(
        final List<HttpMessageConverter<?>> aConverters) {
      aConverters.removeIf(MappingJackson2HttpMessageConverter.class::isInstance);
      final Jackson2ObjectMapperBuilder iBuilder = new Jackson2ObjectMapperBuilder().deserializerByType(String.class,
          new SanitizeDeserializer());
      aConverters.add(new MappingJackson2HttpMessageConverter(iBuilder.build()));
    }
    

    See Spring documentation https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-config/message-converters.html#page-title