Dan Watt
Dan Watt

Reputation: 1477

Handling alternate property names in Jackson Deserialization

I am working on converting a REST/JSON service from Coldfusion 9 to a Spring-MVC 3.1 application. I am using Jackson (1.9.5) and the MappingJacksonJsonConverter that Spring provides, and am customizing the ObjectMapper to name fields with CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES.

The problem I am facing is that our legacy service produces 'camel case to UPPER case with underscores' as json property names. The consumers of this JSON, also written in ColdFusion, could care less about case, but Jackson does care about case, and throws UnrecognizedPropertyExceptions.

After looking into just about every setting I could reach from ObjectMapper - DeserializationConfig, DeserializerProvider, etc, I ended up with a very messy hack in which I parse to a JSON tree, output it with a custom JsonGenerator that lower cases the field names, and then parses it back in as an object.

MappingJacksonHttpMessageConverter mc = new MappingJacksonHttpMessageConverter() {
    @Override
    protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return this.getObjectMapper().readValue(translateToLowerCaseKeys(inputMessage.getBody()), getJavaType(clazz));
    }

    private byte[] translateToLowerCaseKeys(InputStream messageBody) throws IOException {
        StringWriter sw = new StringWriter();
        JsonGenerator lowerCaseFieldNameGenerator = new JsonGeneratorDelegate(this.getObjectMapper().getJsonFactory().createJsonGenerator(sw)) {
            @Override
            public void writeFieldName(String name) throws IOException, org.codehaus.jackson.JsonGenerationException {
                delegate.writeFieldName(name.toLowerCase());
            };
        };
        this.getObjectMapper().writeTree(lowerCaseFieldNameGenerator, this.getObjectMapper().readTree(messageBody));
        lowerCaseFieldNameGenerator.close();
        return sw.getBuffer().toString().getBytes();
    }
};

This solution seems very inefficient. There is a solution that works for keys of a map, but I was unable to find a similar solution for field names.

An alternative solution is to have two setters, one annotated with the legacy field name. The naming strategy has to be extended to ignore these fields, which in my situation is fine since the object mapper won't be dealing with any other classes with an UPPER_UNDERSCORE strategy:

public class JsonNamingTest {
    public static class CaseInsensitive extends LowerCaseWithUnderscoresStrategy {
        public String translate(String in) {
            return (in.toUpperCase().equals(in) ? in : super.translate(in));
        }
    }

    public static class A {
        private String testField;
        public String getTestField() {
            return testField;
        }
        public void setTestField(String field) {
            this.testField = field;
        }
        @JsonProperty("TEST_FIELD")
        public void setFieldAlternate(String field) {
            this.testField = field;
        }
    }

    @Test
    public void something() throws Exception {
        A test = new A();
        test.setTestField("test");
        ObjectMapper mapper = new ObjectMapper().setPropertyNamingStrategy(new CaseInsensitive());
        assertEquals("{\"test_field\":\"test\"}", mapper.writeValueAsString(test));
        assertEquals("test", mapper.readValue("{\"test_field\":\"test\"}", A.class).getTestField());
        assertEquals("test", mapper.readValue("{\"TEST_FIELD\":\"test\"}", A.class).getTestField());
    }
}

This is more ideal than the previous solution, but would require two annotated setters for every field - one for the new format, one to support the legacy format.

Has anyone come across a way to make Jackson case-insensitive for deserializing field names, or for that matter accept multiple aliases for a field name?

Upvotes: 10

Views: 23990

Answers (4)

John Smith
John Smith

Reputation: 96

When using Spring Boot it's also possible to use the Jackson2ObjectMapperBuilderCustomizer which retains the default auto-configuration. This can be done by adding a bean:

@Bean
public Jackson2ObjectMapperBuilderCustomizer acceptCaseInsensitive() {
    return builder -> builder.featuresToEnable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);
}

Upvotes: 1

Dan Watt
Dan Watt

Reputation: 1477

This has been fixed as of Jackson 2.5.0.

ObjectMapper mapper = new ObjectMapper();
mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);

Upvotes: 13

Pascal G&#233;linas
Pascal G&#233;linas

Reputation: 2834

Since you are using Jackson 1.x and not Jackson 2.x, you could use MixIns. From what I understand, you want deserialization to understand UPPER_CASE but you want to serialize in lower_case. Add a MixIn in the deserialization config but not in the serialization config, as such:

public class JsonNamingTest {   
    public static class A {
        private String testField;
        public String getTestField() {
            return testField;
        }
        public void setTestField(String field) {
            this.testField = field;
        }
    }

    public static class MixInA {
        @JsonProperty("TEST_FIELD")
        public void setTestField(String field) {}
    }

    @Test
    public void something() throws Exception {
        A test = new A();
        test.setTestField("test");
        ObjectMapper mapper = new ObjectMapper();
        mapper.getDeserializationConfig().addMixInAnnotations(A.class, MixInA.class);
        assertEquals("{\"test_field\":\"test\"}", mapper.writeValueAsString(test));
        assertEquals("test", mapper.readValue("{\"test_field\":\"test\"}", A.class).getTestField());
        assertEquals("test", mapper.readValue("{\"TEST_FIELD\":\"test\"}", A.class).getTestField());
    }
}

However, you can't do that with Jackson 2.x: mixIns are shared by the serialization and deserialization config. I have yet to find a way to handle aliases with Jackson 2.x :(

Upvotes: 5

Eugene Retunsky
Eugene Retunsky

Reputation: 13139

You can use PropertyNamingStrategy:

  class CaseInsensitiveNaming extends PropertyNamingStrategy {
    @Override
    public String nameForGetterMethod(MapperConfig<?> config,
         AnnotatedMethod method, String defaultName)
    {
      // case-insensitive, underscores etc. mapping to property names
      // so you need to implement the convert method in way you need.
      return convert(defaultName); 
    }
  }

  objectMapper.setPropertyNamingStrategy(new CaseInsensitiveNaming());

Upvotes: 6

Related Questions