简体   繁体   中英

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

You can try to use SimpleModule together with ContextualDeserializer interface. First can be used for wrapping default deserialiser and second for checking type configuration - checking annotation.

Let's start from annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface ForceCustomDeserializer {
}

I assume that you have only one custom implementation for given type but in case it is not true extend above annotation and provide some extra info which allow to use proper deserialisers. For example, below we can see two custom deserialisers which extra logs some info and run default deserialisation. Base deserialiser is used because in case you have some extra configuration we do not loose it.

class CustomBoolDeserializer extends StdScalarDeserializer<Boolean> implements ContextualDeserializer {

    private NumberDeserializers.BooleanDeserializer base;

    public CustomBoolDeserializer(NumberDeserializers.BooleanDeserializer base) {
        super(Boolean.class);
        this.base = base;
    }

    @Override
    public Boolean deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        System.out.println("Custom BooleanDeserializer ....");

        return base.deserialize(p, ctxt);
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
        Class<?> parent = property.getMember().getDeclaringClass();
        ForceCustomDeserializer annotation = parent.getAnnotation(ForceCustomDeserializer.class);

        return annotation == null ? base : this;
    }
}

class CustomStringDeserializer extends StringDeserializer implements ContextualDeserializer {

    private final StringDeserializer base;

    public CustomStringDeserializer(StringDeserializer base) {
        this.base = base;
    }

    @Override
    public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        System.out.println("Custom StringDeserializer ....");

        return base.deserialize(p, ctxt);
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
        Class<?> parent = property.getMember().getDeclaringClass();
        ForceCustomDeserializer annotation = parent.getAnnotation(ForceCustomDeserializer.class);

        return annotation == null ? base : this;
    }
}

We can test above custom implementations as below:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.std.NumberDeserializers;
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;
import com.fasterxml.jackson.databind.deser.std.StringDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

public class JsonApp {

    public static void main(String[] args) throws Exception {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();

        SimpleModule forcedCustomModule = new SimpleModule();
        forcedCustomModule.setDeserializerModifier(new BeanDeserializerModifier() {
            @Override
            public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
                if (deserializer instanceof StringDeserializer) {
                    // wrap with yours or return new deserializer
                    return new CustomStringDeserializer((StringDeserializer) deserializer);
                }
                if (deserializer instanceof NumberDeserializers.BooleanDeserializer) {
                    // wrap with yours or return new deserializer
                    return new CustomBoolDeserializer((NumberDeserializers.BooleanDeserializer) deserializer);
                }
                // override for other types

                return deserializer;
            }
        });

        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(forcedCustomModule);

        System.out.println(mapper.readValue(jsonFile, Pojo.class));
    }
}

@ForceCustomDeserializer
class Pojo {

    private String name;
    private boolean bool;

    // getters, setters, toString
}

Above example for below JSON payload:

{
  "name": "Jackson",
  "bool": true
}

prints:

Custom StringDeserializer ....
Custom BooleanDeserializer ....
Pojo{name='Jackson', bool=true}

See also:

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM