The no-breaking-change API

Ever thought of changing the API contract without having to ask for any upgrade ?
Without waiting for all the consumers to adapt their client ?

There is one technique that allow you to build backward compatible contracts, and it does not need versioning nor any deprecation strategy.

What is it ? Just make good use of data unmarshalling.

The below example accept flexible representations of the same data by just accepting several formats for the same object : either as a simple value, as a list of values, or as an object containing a list of values.
… Just look at the main method at the end of this snippet : the 4 examples produce exactly the same object.

package org.toilelibre.libe.curl;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

import java.util.List;
import java.util.Objects;
import java.util.UUID;

import static java.util.Collections.singletonList;

public class TestUnmarshal {
    static class A {
        List<B> b;

        public A() {

        public A(List<B> b) {
            this.b = b;

        public boolean equals(Object o) {
            return Objects.equals(b, ((A) o).b);

        public int hashCode() {
            return Objects.hash(b);

    static class B {
        UUID id;

    private static ObjectMapper objectMapper =
        .deserializerByType(A.class, new JsonDeserializer<A>() {
                        public A deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
                            final TreeNode treeNode = p.readValueAsTree();
                            final ArrayNode arrayNode = treeNode.isObject() && treeNode.size() == 1 &&
                                    ((ObjectNode) treeNode).elements().next().isArray() ?
                                    (ArrayNode) treeNode.get(0) : treeNode.isArray() ? (ArrayNode) treeNode : null;
                            if (arrayNode != null) {
                                return new A(
                              , false)
                                                .map(node -> {
                                                    try {
                                                        return p.getCodec().treeToValue(node, B.class);
                                                    } catch (JsonProcessingException e) {
                                                        return null;
                            return new A(singletonList(p.getCodec().treeToValue(treeNode, B.class)));

    public static void main(String[] args) throws IOException {
        A a1 = objectMapper.readValue("{\"_id\":\"00000000-0000-0000-0000-000000000000\"}", A.class);
        A a2 = objectMapper.readValue("[{\"_id\":\"00000000-0000-0000-0000-000000000000\"}]", A.class);
        A a3 = objectMapper.readValue("{\"b\":[{\"_id\":\"00000000-0000-0000-0000-000000000000\"}]}", A.class);
        A a4 = objectMapper.readValue("{\"listOfBValues\":[{\"_id\":\"00000000-0000-0000-0000-000000000000\"}]}", A.class);
        assert Objects.equals(a1, a2);
        assert Objects.equals(a2, a3);
        assert Objects.equals(a3, a4);

Leave a Reply

Your email address will not be published. Required fields are marked *


This site uses Akismet to reduce spam. Learn how your comment data is processed.