Niranjan
Niranjan

Reputation: 2931

Jackson 2 support for versioning

Does anyone know if Jackson2 supports versioning; something similar to GSON's @Since and @Until annotations?

Upvotes: 9

Views: 7963

Answers (3)

user2636594
user2636594

Reputation:

My solution is an interface with default methods: getModelVersion() and migrateModel(String fromVersion, JsonNode jsonNode).

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.BeanDeserializerFactory;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.ResolvableDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.easy.refund.exception.BadRequestException;

import java.io.IOException;

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonDeserialize(using = VersionedModel.VersionedDeserializer.class)
public interface VersionedModel {
    String MODEL_VERSION_PROPERTY_NAME = "@modelVersion";

    /**
     * Override to increase current version.
     */
    @JsonProperty(value = MODEL_VERSION_PROPERTY_NAME)
    default String getModelVersion() {
        return "1";
    }

    /**
     * Invoked on version update after deserialization.
     */
    default void migrateModel(String fromVersion, JsonNode jsonNode) {
        throw new BadRequestException(String.format("Unexpected model version %s instead of %s.", fromVersion, getModelVersion()));
    }

    class VersionedDeserializer extends StdDeserializer<VersionedModel> implements ContextualDeserializer {

        public VersionedDeserializer(JavaType type) {
            super(type);
        }

        public VersionedDeserializer() {
            this(null);
        }


        @Override
        public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
            return new VersionedDeserializer(ctxt.getContextualType());
        }

        @Override
        public VersionedModel deserialize(final JsonParser jp, final DeserializationContext ctxt) throws IOException, JsonProcessingException {
            ObjectCodec oc = jp.getCodec();
            JsonNode node = oc.readTree(jp);

            DeserializationConfig config = ctxt.getConfig();
            JavaType type = getValueType();
            JsonDeserializer<Object> defaultDeserializer = BeanDeserializerFactory.instance.buildBeanDeserializer(ctxt, type, config.introspect(type));

            if (defaultDeserializer instanceof ResolvableDeserializer) {
                ((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
            }

            JsonParser treeParser = oc.treeAsTokens(node);
            config.initialize(treeParser);

            if (treeParser.getCurrentToken() == null) {
                treeParser.nextToken();
            }

            Object deserialized = defaultDeserializer.deserialize(treeParser, ctxt);

            if (deserialized instanceof VersionedModel versionedModel) {
                String fromVersion = node.get(MODEL_VERSION_PROPERTY_NAME).asText();
                if (!versionedModel.getModelVersion().equals(fromVersion)) {
                    versionedModel.migrateModel(fromVersion, node);
                }
                return versionedModel;
            }

            throw new RuntimeException("Unexpected type: " + deserialized.getClass().getName());
        }
    }
}

Upvotes: 0

Jon Peterson
Jon Peterson

Reputation: 2996

The Jackson Model Versioning Module adds versioning support which satisfies a super-set of GSON's @Since and @Until.


Lets say you have a GSON-annotated model:

public class Car {
    public String model;
    public int year;
    @Until(1) public String new;
    @Since(2) public boolean used;
}

Using the module, you could convert it to the following Jackson class-level annotation...

@JsonVersionedModel(currentVersion = '3', toCurrentConverterClass = ToCurrentCarConverter)
public class Car {
    public String model;
    public int year;
    public boolean used;
}

...and write a to-current-version converter:

public class ToCurrentCarConverter implements VersionedModelConverter {
    @Override
    public ObjectNode convert(ObjectNode modelData, String modelVersion,
                              String targetModelVersion, JsonNodeFactory nodeFactory) {

        // model version is an int
        int version = Integer.parse(modelVersion);

        // version 1 had a 'new' text field instead of a boolean 'used' field
        if(version <= 1)
            modelData.put("used", !Boolean.parseBoolean(modelData.remove("new").asText()));
    }
}

Now just configure the Jackson ObjectMapper with the module and test it out.

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

// version 1 JSON -> POJO
Car hondaCivic = mapper.readValue(
    "{\"model\": \"honda:civic\", \"year\": 2016, \"new\": \"true\", \"modelVersion\": \"1\"}",
    Car.class
)

// POJO -> version 2 JSON
System.out.println(mapper.writeValueAsString(hondaCivic))
// prints '{"model": "honda:civic", "year": 2016, "used": false, "modelVersion": "2"}'

Disclaimer: I am the author of this module. See the GitHub project page for more example of additional functionality. I have also written a Spring MVC ResponseBodyAdvise for using the module.

Upvotes: 10

StaxMan
StaxMan

Reputation: 116620

Not directly. You could use @JsonView or JSON Filter functionality for implementing similar inclusion/exclusion.

Upvotes: 2

Related Questions