2

I'm working on Jackson configuration and I wonder if there is any option to deserialise different kinds of field patterns.

For example, I have an object:

class DeserializeIt {
    String fieldOne;
    String fieldOneAndHalf;
    String fieldTwo;
    String fieldThree;
    String fieldFour;

   //getters setters etc.
}

And I have below JSON payload:

{
    "fieldOne" : "value1",
    "field_ONE-and_Half": "value15",
    "FIELD_TWO": "value2",
    "FIELD_THREE" : "value3",
    "field_four": "value4"
}

I would like to deserialize all these field names to camel case without an exception.

I tried to create my custom PropertyNamingStrategy but it goes from another direction: it does not convert delimitered fields to camel case, it tries to convert the objects fields and search for them in the parsed string.

And since I cannot pass a list of possible strings instead of one variation (fieldOne can become field-one, field_one, field-ONE etc.), this does not work.

Do you know what else could I configure for such a relaxed deserialization?

Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
youngDev
  • 329
  • 3
  • 16
  • I'm not sure Jackson provides something for that out of the box but I'd try either providing a custom deserializer that converts the property names before defering to one of the existing or (which might be easier but a little less efficient) read the json into the generic tree format (a bunch of nested `JsonNode` objects), convert the map keys (remove, convert and put again), reformat the tree to JSON and finally have Jackson parse the reformatted json into the POJO. – Thomas Apr 25 '19 at 10:15

3 Answers3

1

We need to extend com.fasterxml.jackson.databind.deser.BeanDeserializerModifier and com.fasterxml.jackson.databind.deser.BeanDeserializer which deserialises POJO classes. Below solution depends from version you are using because I copied some code from base class which is not ready for intercepting extra functionality. If you do not have any extra configuration for your POJO classes vanillaDeserialize method will be invoked and this one we will try to improve.

In other case you need to debug this deserialiser and updated other places if needed. Below solution uses version 2.9.8.

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.JsonTokenId;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.BeanDeserializer;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBase;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import com.fasterxml.jackson.databind.module.SimpleModule;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class JsonApp {

    public static void main(String[] args) throws Exception {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();

        SimpleModule relaxedModule = new SimpleModule();
        relaxedModule.setDeserializerModifier(new RelaxedBeanDeserializerModifier());

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

        System.out.println(mapper.readValue(jsonFile, DeserializeIt.class));
    }
}

class RelaxedBeanDeserializerModifier extends BeanDeserializerModifier {

    @Override
    public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
        JsonDeserializer<?> base = super.modifyDeserializer(config, beanDesc, deserializer);
        if (base instanceof BeanDeserializer) {
            return new RelaxedBeanDeserializer((BeanDeserializer) base);
        }

        return base;
    }
}

class RelaxedBeanDeserializer extends BeanDeserializer {

    private Map<String, String> properties = new HashMap<>();

    public RelaxedBeanDeserializer(BeanDeserializerBase src) {
        super(src);
        _beanProperties.forEach(property -> {
            properties.put(property.getName().toLowerCase(), property.getName());
        });
    }

    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        // common case first
        if (p.isExpectedStartObjectToken()) {
            if (_vanillaProcessing) {
                return vanillaDeserialize(p, ctxt, p.nextToken());
            }
            // 23-Sep-2015, tatu: This is wrong at some many levels, but for now... it is
            //    what it is, including "expected behavior".
            p.nextToken();
            if (_objectIdReader != null) {
                return deserializeWithObjectId(p, ctxt);
            }
            return deserializeFromObject(p, ctxt);
        }
        return _deserializeOther(p, ctxt, p.getCurrentToken());
    }

    protected Object vanillaDeserialize(JsonParser p, DeserializationContext ctxt, JsonToken t) throws IOException {
        final Object bean = _valueInstantiator.createUsingDefault(ctxt);
        // [databind#631]: Assign current value, to be accessible by custom serializers
        p.setCurrentValue(bean);

        if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
            String propName = p.getCurrentName();

            do {
                String relaxedName = getRelaxedName(propName);
                String mappedName = properties.get(relaxedName);
                defaultImplementation(p, ctxt, bean, mappedName);
            } while ((propName = p.nextFieldName()) != null);
        }
        return bean;
    }

    private void defaultImplementation(JsonParser p, DeserializationContext ctxt, Object bean, String propName) throws IOException {
        p.nextToken();
        SettableBeanProperty prop = _beanProperties.find(propName);

        if (prop != null) { // normal case
            try {
                prop.deserializeAndSet(p, ctxt, bean);
            } catch (Exception e) {
                wrapAndThrow(e, bean, propName, ctxt);
            }
            return;
        }
        handleUnknownVanilla(p, ctxt, bean, propName);
    }

    private String getRelaxedName(String name) {
        return name.replaceAll("[_\\-]", "").toLowerCase();
    }
}

Above code prints:

DeserializeIt{fieldOne='value1', fieldOneAndHalf='value15', fieldTwo='value2', fieldThree='value3', fieldFour='value4'}

See also:

Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
0

From Jackson 2.9, you can provide multiple possible properties names for deserialization using the @JsonAlias annotation. On your example, it would be like this:

class DeserializeIt {
  @JsonAlias("fieldOne") 
  String fieldOne;

  @JsonAlias("field_ONE-and_Half") 
  String fieldOneAndHalf;

  @JsonAlias("FIELD_TWO") 
  String fieldTwo;

  @JsonAlias("FIELD_THREE") 
  String fieldThree;
  // and so on...
}
viniciusjssouza
  • 1,235
  • 14
  • 28
  • 1
    Relaxed in my sense means that one field may look differently: like fieldOne, field-one, field_ONE etc. so actually i don't know exactly what the consumer sends. And it is hard to encount all the possibilities for all the fields. – youngDev Apr 26 '19 at 07:14
0

What worked for myself: I added an AOP component that renames all the fields of incoming object into the Camel case.

youngDev
  • 329
  • 3
  • 16