Search code examples
javaspringspring-bootjacksondeserialization

Is it possible to configure Jackson custom deserializers at class level for different data types?


I need to deserialize a long and complex json for which I wrote a set of java classes to map the data, and I had to write custom deserializers for many fields of different types (including String, Boolean, BigDecimal, etc.).

I know I can annotate all fields in the java classes with the corresponding custom deserializer (like below), but then I would need to annotate almost all the fields in all the classes.

@JsonDeserialize(using = CustomBooleanJsonDeserializer.class)
private boolean active;

I also know that I can register a module in the Spring default ObjectMapper (like here), but I just want to use these custom deserializers for these specific classes.

@Bean
public Module customDeserializersModule() {
    SimpleModule module = new SimpleModule();
    module.addDeserializer(Boolean.class, new CustomBooleanJsonDeserializer());
    // add other custom deserializers 
    return module;
}

I even know that I can use a custom ObjectMapper in the RestController, but I don't want to give up the convenience of automatic data binding via @RequestBody, because I must prevent others from using this without the necessary custom deserializers.

@RequestMapping(method = RequestMethod.POST, value = "/data")
public ResponseEntity<ServerInfo> register(@RequestBody DataMapper data) {
   // DataMapper is the target POJO class of the json's deserialization
}

In short, I'm looking for something like this at class level:

@JsonDeserialize(using = CustomStringJsonDeserializer.class, forType = String.class)
@JsonDeserialize(using = CustomBooleanJsonDeserializer.class, forType = Boolean.class)
@JsonDeserialize(using = CustomBigDecimalJsonDeserializer.class, forType = BigDecimal.class)
public class DataMapper implements Serializable {
    // obviously, @JsonDeserialize doesn't have a forType method
}

or maybe some way to implement a custom deserializer for the DataMapper class, that defines how to deserialize each field according to its data type (without having to annotate each field):

@JsonDeserialize(using = DataMapperJsonDeserializer.class)
public class DataMapper implements Serializable {
    // How can I implement the DataMapperJsonDeserializer with these 
    // characteristics? I know about the ContextualDeserializer interface, 
    // but I don't know how to use it without annotating each field.
}

or some way of restricting the effect of a module to just one package or set of classes:

module.restrictedTo(/*some package or set of classes*/);
// com.fasterxml.jackson.databind.Module doesn't have a restrictedTo method

Solution

  • You can define a custom deserializer for the class (as the second idea in the question) and use your own custom ObjectMapper inside:

    public class DataMapperJsonDeserializer extends JsonDeserializer<DataMapper> {
    
        private static final ObjectMapper objectMapper = new ObjectMapper();
        private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd/MM/yyyy");
    
        static {
            SimpleModule module = new SimpleModule();
            module.addDeserializer(BigInteger.class, new CustomBigIntegerJsonDeserializer());
            module.addDeserializer(BigDecimal.class, new CustomBigDecimalJsonDeserializer());
            module.addDeserializer(Boolean.class, new CustomBooleanJsonDeserializer());
            module.addDeserializer(String.class, new CustomStringJsonDeserializer());
            objectMapper.registerModule(module);
            objectMapper.addMixIn(DataMapper.class, DefaultJsonDeserializer.class);
            objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            objectMapper.setDateFormat(simpleDateFormat);
        }
    
        @Override
        public DataMapper deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
            return objectMapper.readValue(jsonParser, DataMapper.class);
        }
    
        @JsonDeserialize
        private interface DefaultJsonDeserializer {
            // Reset default json deserializer
        }
    
    }
    

    Note the use of Jackson Mix-in Annotations (the DefaultJsonDeserializer interface) to dynamically remove the custom deserializer from the POJO class, avoiding the StackOverflowError that would otherwise be thrown as a result of objectMapper.readValue(jsonParser, DataMapper.class).


    Then, it's just to annotate the POJO class:

    @JsonDeserialize(using = DataMapperJsonDeserializer.class)
    public class DataMapper implements Serializable {
        // It is not necessary to annotate each field with custom deserializers.
    }
    

    You can even add other POJO classes as fields of DataMapper and the custom deserializers for each type will be automatically applied to its fields, without need for annotations.