4

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
lcnicolau
  • 3,252
  • 4
  • 36
  • 53

2 Answers2

6

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.

lcnicolau
  • 3,252
  • 4
  • 36
  • 53
  • Using new customized `ObjectMapper` instance in custom deserialiser does the trick. If you have tested it and it works well for you in for all corner cases, I think, it is acceptable. Trick with `@JsonDeserialize` annotation is very interesting. I was not aware about it. One cons, that this solution does not scale well for many `POJO` classes. You need to create many deserialisers with hidden `ObjectMappers`. – Michał Ziober Apr 04 '19 at 15:39
  • @MichałZiober: I edited my answer, you can use this approach to create different levels of nesting without creating a custom deserializer for each `POJO` class. On the other hand, the idea (_as you suggested_) is​ configuring an `ObjectMapper` globally, and if necessary, to overwrite it and use a custom `ObjectMapper`, through a simple annotation at the class level. – lcnicolau Apr 05 '19 at 15:22
3

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:

Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
  • I like it, although as a last resort. To use this approach through the default `@RequestBody` data binding in Spring Controllers I would need to register this module globally, therefore, the system would verify the existence of the `@ForceCustomDeserializer` annotation for all deserializations, and it is a big system. – lcnicolau Apr 02 '19 at 15:26
  • **Is there any way to do it without having to register this module before the deserialization?** That is, that the `POJO` tells to the `ObjectMapper` how it should be deserialized and not that the `ObjectMapper` has to verify all the `POJOs`. I have no problem with the `BeanDeserializerModifier`, but I would prefer to avoid defining it globally. – lcnicolau Apr 02 '19 at 15:26
  • @Nicolau, you want to set custom deserialisers for given `POJO` classes. And now we have two ways: annotate with `Jackson` annotations and `Jackson` will read these annotation and assign custom deserialisers for given properties **or** tell `Jackson` in more general way - using `BeanDeserializerModifier`. Please, notice that above example checks in custom deserialisers whether `@ForceCustomDeserializer` annotation is used on class level or not. If annotation exists - custom deserialiser is returned; if not - implementation peeks default deserialiser. – Michał Ziober Apr 02 '19 at 15:41
  • Yes, this is a great example, and I think that in a certain way it matches my third idea: _restricting the effect of a module to just one package or set of classes_. I just wish I could set custom deserializers for certain `POJO` classes, only with `Jackson` annotations but without annotating each field. Something at the class level, like the first two ideas in the question. **Is there any way to register a similar** `BeanDeserializerModifier` **within a custom deserializer, applicable at the class level?** – lcnicolau Apr 02 '19 at 18:00
  • @Nicolau, I didn't found annotation like this. [@JsonDeserialize](https://fasterxml.github.io/jackson-databind/javadoc/2.9/com/fasterxml/jackson/databind/annotation/JsonDeserialize.html) works on given method, field or class **directly**. We can use [ObjectMapper.configOverride(java.lang.Class>)](https://fasterxml.github.io/jackson-databind/javadoc/2.9/com/fasterxml/jackson/databind/ObjectMapper.html#configOverride-java.lang.Class-) but it works for all classes. Maybe, you could create two `ObjectMapper` instances and use one instance when you need custom deserialiser s and other otherwise. – Michał Ziober Apr 02 '19 at 19:54
  • Can you please take a look at [my answer](https://stackoverflow.com/a/55517987/4071001) and give me your opinion? I think I found a simpler solution by defining a custom deserializer for the `POJO` class. Can you see any inconvenience of doing it this way? Thank you very much for your time. – lcnicolau Apr 04 '19 at 14:29