How can I loosen up the naming strategy when deserializing using Jackson?
Asked Answered
L

2

10

I've been trying to upgrade the JSON modules to use the FasterXML (2.6.3) versions of Jackson instead of the old Codehaus modules. During the upgrade, I've noticed that the naming strategy differs when using FasterXML instead of Codehaus.

Codehaus was more flexible when it came to the naming strategy. The test below highlights the issue I'm facing with FasterXML. How can I configure the ObjectMapper so it follows the same strategy like Codehaus?

I cannot alter the JSONProperty annotations as there are hundreds of them. I would like the upgrade to be backwards compatible with respect to the naming strategy.

import java.io.IOException;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
/*import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.annotate.JsonProperty;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.PropertyNamingStrategy;*/
import org.junit.Assert;
import org.junit.Test;

public class JSONTest extends Assert {

    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class Product {

        @JsonProperty(value = "variationId")
        private String variantId;

        @JsonProperty(value = "price_text")
        private String priceText;

        @JsonProperty(value = "listPrice")
        public String listPrice;

        @JsonProperty(value = "PRODUCT_NAME")
        public String name;

        @JsonProperty(value = "Product_Desc")
        public String description;
    }

    private static final String VALID_PRODUCT_JSON =
            "{ \"list_price\": 289," +
             " \"price_text\": \"269.00\"," +
             " \"variation_id\": \"EUR\"," +
             " \"product_name\": \"Product\"," +
             " \"product_desc\": \"Test\"" +
            "}";

    @Test
    public void testDeserialization() throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);

        Product product = mapper.readValue(VALID_PRODUCT_JSON, Product.class);
        System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(product));
        assertNotNull(product.listPrice);
        assertNotNull(product.variantId);
        assertNotNull(product.priceText);
        assertNotNull(product.name);
        assertNotNull(product.description);
    }
}
Logarithmic answered 13/11, 2015 at 10:39 Comment(1)
JSON is case-sensitive; keys with different casing represent different things. It appears the Codehaus version was noncompliant in this regard.Ordovician
R
10

@JsonProperty overrides any PropertyNamingStrategy in fasterxml since version 2.4.0. However, yet-to-be-released version 2.7.0 will provide a feature to allow you to opt back in to the old behavior. There is also an unimplemented suggestion to toggle this at the per-annotation level, but that would not really help you.

It appears that Codehaus does apply the PropertyNamingStrategy on top of the @JsonProperty values when mapping, although I can't find any clear docs on that. This appears to have been the behavior in fasterxml before 2.4.0 as well. Here is another example of someone noticing the same difference in behavior.

Ronaronal answered 16/11, 2015 at 21:46 Comment(0)
D
2

Although the solution provided by SkinnyJ is perfect for your problem, but if you can't wait till 2.7 is released, you can apply the below hack to get around the problem.

The idea is to transform the incoming JSON to match the attributes in your bean definition. Below code does that. Following points should be noted:

  1. If you are dealing with nested structures, you will have to implement a recursive function to achieve this transformation.
  2. There is a little overhead involved in doing the transformation.

Code:

public class JSONTest extends Assert {

    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class Product {

        @JsonProperty(value = "variationId")
        private String variantId;

        @JsonProperty(value = "price_text")
        private String priceText;

        @JsonProperty(value = "listPrice")
        public String listPrice;

        @JsonProperty(value = "PRODUCT_NAME")
        public String name;

        @JsonProperty(value = "Product_Desc")
        public String description;
    }

    private static final String VALID_PRODUCT_JSON =
            "{ \"list_price\": 289," +
             " \"price_text\": \"269.00\"," +
             " \"variation_id\": \"EUR\"," +
             " \"product_name\": \"Product\"," +
             " \"product_desc\": \"Test\"" +
            "}";

    @Test
    public void testDeserialization() throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);

        //Capture the original JSON in org.json.JSONObject
        JSONObject obj = new JSONObject(VALID_PRODUCT_JSON);
        JSONArray keys = obj.names();

        //New json object to be created using property names defined in bean
        JSONObject matchingJson = new JSONObject();

        //Map of lowercased key to original keys in incoming json. eg: Prod_id > prodid
        Map<String, String> jsonMappings = new LinkedHashMap<String, String>();
        for (int i = 0; i < keys.length(); i++) {
            String key = lowerCaseWithoutUnderScore(keys.getString(i));
            String value = keys.getString(i);
            jsonMappings.put(key, value);
        }

        /*
         * Iternate all jsonproperty beans and create new json
         * such that keys in json map to that defined in bean
         */
        Field[] fields = Product.class.getDeclaredFields();
        for (Field field : fields) {
            JsonProperty prop = field.getAnnotation(JsonProperty.class);
            String propNameInBean = prop.value();
            String keyToLook = lowerCaseWithoutUnderScore(propNameInBean);
            String keyInJson = jsonMappings.get(keyToLook);
            matchingJson.put(propNameInBean, obj.get(keyInJson));
        }

        String json = matchingJson.toString();
        System.out.println(json);

        //Pass the matching json to Object mapper
        Product product = mapper.readValue(json, Product.class);
        System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(product));
        assertNotNull(product.listPrice);
        assertNotNull(product.variantId);
        assertNotNull(product.priceText);
        assertNotNull(product.name);
        assertNotNull(product.description);
    }

    private String lowerCaseWithoutUnderScore(String key){
        return key.replaceAll("_", "").toLowerCase();
    }

}
Digression answered 23/11, 2015 at 4:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.