'Restructure JSON before deserializing with Jackson

We have a service which currently consumes JSON. We want to slightly restructure this JSON (move one property one level up) but also implement graceful migration so that our service could process old structure as well as new structure. We're using Jackson for JSON deserialization.

How do we restructure JSON prior to deserialization with Jackson?

Here's a MCVE.

Assume our old JSON looks as follows:

{"reference": {"number" : "one", "startDate" : [2016, 11, 16], "serviceId" : "0815"}}

We want to move serviceId one level up:

{"reference": {"number" : "one", "startDate" : [2016, 11, 16]}, "serviceId" : "0815"}

This are the classes we want to deserialize from both old an new JSONs:

   public final static class Container {

        public final Reference reference;

        public final String serviceId;

        @JsonCreator
        public Container(@JsonProperty("reference") Reference reference, @JsonProperty("serviceId") String serviceId) {
            this.reference = reference;
            this.serviceId = serviceId;
        }

    }

    public final static class Reference {

        public final String number;

        public final LocalDate startDate;

        @JsonCreator
        public Reference(@JsonProperty("number") String number, @JsonProperty("startDate") LocalDate startDate) {
            this.number = number;
            this.startDate = startDate;
        }
    }

We only want serviceId in Container, not in both classes.

What I've got working is the following deserializer:

public static class ServiceIdMigratingContainerDeserializer extends JsonDeserializer<Container> {

    private final ObjectMapper objectMapper;

    {
        objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    @Override
    public Container deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        ObjectNode node = p.readValueAsTree();
        migrate(node);
        return objectMapper.treeToValue(node, Container.class);
    }

    private void migrate(ObjectNode containerNode) {
        TreeNode referenceNode = containerNode.get("reference");
        if (referenceNode != null && referenceNode.isObject()) {
            TreeNode serviceIdNode = containerNode.get("serviceId");
            if (serviceIdNode == null) {
                TreeNode referenceServiceIdNode = referenceNode.get("serviceId");
                if (referenceServiceIdNode != null && referenceServiceIdNode.isValueNode()) {
                    containerNode.set("serviceId", (ValueNode) referenceServiceIdNode);
                }
            }
        }
    }
}

This deserializer first retrieves the tree, manipulates it and then deserializers it using an own instance of ObjectMapper. It works but we really dislike the fact that we have another instance of ObjectMapper here. If we don't create it and somehow use the system-wide instance of ObjectMapper we get an infinite cycle because when we try to call objectMapper.treeToValue, our deserializer gets called recursively. So this works (with an own instance of ObjectMapper) but it is not an optimal solution.

Another method I've tried was using a BeanDeserializerModifier and a own JsonDeserializer which "wraps" the default serializer:

public static class ServiceIdMigrationBeanDeserializerModifier extends BeanDeserializerModifier {

    @Override
    public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc,
            JsonDeserializer<?> defaultDeserializer) {
        if (beanDesc.getBeanClass() == Container.class) {
            return new ModifiedServiceIdMigratingContainerDeserializer((JsonDeserializer<Container>) defaultDeserializer);
        } else {
            return defaultDeserializer;
        }
    }
}

public static class ModifiedServiceIdMigratingContainerDeserializer extends JsonDeserializer<Container> {

    private final JsonDeserializer<Container> defaultDeserializer;

    public ModifiedServiceIdMigratingContainerDeserializer(JsonDeserializer<Container> defaultDeserializer) {
        this.defaultDeserializer = defaultDeserializer;
    }

    @Override
    public Container deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        ObjectNode node = p.readValueAsTree();
        migrate(node);
        return defaultDeserializer.deserialize(new TreeTraversingParser(node, p.getCodec()), ctxt);
    }

    private void migrate(ObjectNode containerNode) {
        TreeNode referenceNode = containerNode.get("reference");
        if (referenceNode != null && referenceNode.isObject()) {
            TreeNode serviceIdNode = containerNode.get("serviceId");
            if (serviceIdNode == null) {
                TreeNode referenceServiceIdNode = referenceNode.get("serviceId");
                if (referenceServiceIdNode != null && referenceServiceIdNode.isValueNode()) {
                    containerNode.set("serviceId", (ValueNode) referenceServiceIdNode);
                }
            }
        }
    }
}

"Wrapping" a default deserializer seems to be a better approach, but this fails with an NPE:

java.lang.NullPointerException
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:157)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:150)
    at de.db.vz.rikernpushadapter.migration.ServiceIdMigrationTest$ModifiedServiceIdMigratingContainerDeserializer.deserialize(ServiceIdMigrationTest.java:235)
    at de.db.vz.rikernpushadapter.migration.ServiceIdMigrationTest$ModifiedServiceIdMigratingContainerDeserializer.deserialize(ServiceIdMigrationTest.java:1)
    at com.fasterxml.jackson.databind.ObjectReader._bindAndClose(ObjectReader.java:1623)
    at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:1217)
    at ...

The whole MCVE code is in the following PasteBin. It is a single-class all-containing test case which demonstrates both approaches. The migratesViaDeserializerModifierAndUnmarshalsServiceId fails.

So this leaves me with a question:

How do we restructure JSON prior to deserialization with Jackson?



Solution 1:[1]

In the best traditions, right after posting the question, I've managed to solve this.

Two things:

  • I had to do newJsonParser.nextToken(); to avoid NPE.
  • Extend DelegatingDeserializer

Here's a working DelegatingDeserializer:

public static class ModifiedServiceIdMigratingContainerDeserializer 
                                             extends DelegatingDeserializer {

    public ModifiedServiceIdMigratingContainerDeserializer(JsonDeserializer<?> defaultDeserializer) {
        super(defaultDeserializer);
    }

    @Override
    protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {
        return new ModifiedServiceIdMigratingContainerDeserializer(newDelegatee);
    }

    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        return super.deserialize(restructure(p), ctxt);
    }

    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt, Object intoValue) throws IOException,
            JsonProcessingException {
        return super.deserialize(restructure(p), ctxt, intoValue);
    }

    public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer typeDeserializer)
            throws IOException, JsonProcessingException {
        return super.deserializeWithType(restructure(jp), ctxt, typeDeserializer);
    }

    public JsonParser restructure(JsonParser p) throws IOException, JsonParseException {
        final ObjectNode node = p.readValueAsTree();
        migrate(node);
        final TreeTraversingParser newJsonParser = new TreeTraversingParser(node, p.getCodec());
        newJsonParser.nextToken();
        return newJsonParser;
    }

    private void migrate(ObjectNode containerNode) {
        TreeNode referenceNode = containerNode.get("reference");
        if (referenceNode != null && referenceNode.isObject()) {
            TreeNode serviceIdNode = containerNode.get("serviceId");
            if (serviceIdNode == null) {
                TreeNode referenceServiceIdNode = referenceNode.get("serviceId");
                if (referenceServiceIdNode != null && referenceServiceIdNode.isValueNode()) {
                    containerNode.set("serviceId", (ValueNode) referenceServiceIdNode);
                }
            }
        }
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 JeanValjean