Can a non-nested value be deserialized using a custom Jackson JsonDeserializer?

Viewed 160

I'm working with a project that is using a custom Jackson JsonDeserializer to deserialize an object with a field containing a third-party type from JSON that cannot be deserialized directly. This deserializer is used by annotating the field with @JsonDeserialize(using = CustomTypeDeserializer.class).

public static class OuterType {
    @JsonDeserialize(using = CustomTypeDeserializer.class)
    private CustomType customType;

    public CustomType getCustomType() { return customType; }
    public void setCustomType(CustomType customType) { this.customType = customType; }
}

This works fine when the type to be deserialized is a property of another type, since there is a place to put the @JsonDeserialize annotation. One thing it does not work for is deserializing the type directly.

new ObjectMapper().readValue(json, CustomType.class); // Doesn't work.

It doesn't look like I can use the deserializer directly, as it does not appear to be written in a way that it can directly deserialize values outside of the context of an ObjectMapper.

Note that the type to be deserialized is a third-party type, and therefore I cannot modify it with Jackson annotations.

Is there a way to deserialize a non-nested type using a custom deserializer?

3 Answers

One approach would be to write a custom Module containing the deserializer to the module, and add the module to the ObjectMapper.

public static class CustomTypeModule extends SimpleModule {
    public CustomTypeModule() {
        addDeserializer(CustomType.class, new CustomTypeDeserializer());
    }
}
JsonMapper objectMapper = JsonMapper.builder().addModule(new CustomTypeModule()).build();
CustomType value = objectMapper.readValue(json, CustomType.class);

If you don't want to modify your global application ObjectMapper (making the module available to all requests), you can apply this to a copy of the ObjectMapper used just for this operation.

JsonMapper objectMapper = JsonMapper.builder().build();

// ...

JsonMapper customTypeMapper = objectMapper.rebuild().addModule(new CustomTypeModule()).build();
// OR
ObjectMapper customTypeMapper = objectMapper.copy().registerModule(new CustomTypeModule())

CustomType value = customTypeMapper.readValue(json, CustomType.class);

If this were not a third-party type, the type itself could be annotated with the @JsonDeserialize annotation. Since the type is externally controlled and cannot be modified, this approach cannot be used directly.

However, a Jackson mixin can be used to configure the ObjectMapper (or a copy of it) to treat the type as though the @JsonDeserialize annotation were present on it:

@JsonDeserialize(using = CustomTypeDeserializer.class)
public abstract class CustomTypeMixin {}
JsonMapper objectMapper = JsonMapper.builder()
        .addMixIn(CustomType.class, CustomTypeMixin.class).build();
CustomType value = objectMapper.readValue(json, CustomType.class));

If yours possible scenarios guarantee that a custom deserialer will be always applied over your type not considering if it is nested or not you could apply directly the JsonDeserialize annotation over your CustomType class as an alternative to your solution :

@JsonDeserialize(using = CustomTypeDeserializer.class)
public class CustomType {}

If in your json you want to extract the CustomType object you can use the readTree and treeToValue methods :

//customType object labelled with "customType"
JsonNode tree = mapper.readTree(src).get("customType");
CustomType custom = mapper.treeToValue(tree, CustomType.class);
Related