Search code examples
jsonjacksonjackson-databind

How to deserialize single json property into multiple Java fields (if possible with converter)


Having this class:

@Getter
@Setter
public class Result {

    private String positionText;
    private Integer positionNumber;
    .. many many other properties ..
}

and deserializing this json:

[
    {
        "position": "1",
        .. many many other properties ..
    },
    {
        "position": "FOO",
        .. many many other properties ..
    },
    ..
}

how can the position json property deserialized into both the positionText and positionNumber Java fields?

public abstract class ResultMixIn {

    @JsonProperty("position")
    abstract String getPositionText();

    @JsonProperty("position")
    abstract Integer getPositionNumber();
}

but this gives a:

Conflicting getter definitions for property "position": com.example.domain.Result#getPositionText() vs com.example.domain.Result#getPositionNumber()

Also changing the abstract getters to setters does not make a difference.

If possible I would like to avoid a fully fledged ResultDeserializer extending StdDeserializer as the Result class has many more properties which I would prefer not to deserialize "by hand".

PS: I'm not concerned about serializing. I'm only deserializing the model.


Solution

  • First you need to annotate the properties of the Result class, so that Jackson will deserialize the positionText property, but not the positionNumber. You will do the latter by yourself in a taylor-made deserializer.

    @Getter
    @Setter
    public class Result {
    
        @JsonProperty("position")
        private String positionText;
        
        @JsonIgnore
        private Integer positionNumber;
        
        .. many many other properties ..
    }
    

    By default Jackson would use a BeanDeserializer for deserializing Result objects. But you want a slightly modified implementation of this deserializer.

    The rest of this answer is largely an adaptation of the accepted answer given to the question How do I call the default deserializer from a custom deserializer in Jackson.

    As usual your deserializer extends from StdDeserializer<Result>, but it also implements the ResolvableDeserializer interface. In the deserialize method most of the work is delegated to the default deserializer (in this case a BeanDeserializer) which we got from Jackson. We only add a small extra logic for setting the positionNumber property based on the positionText property.

    public class ResultDeserializer extends StdDeserializer<Result> implements ResolvableDeserializer {
    
        private final JsonDeserializer<?> defaultDeserializer;
    
        public ResultDeserializer(JsonDeserializer<?> defaultDeserializer) {
            super(Result.class);
            this.defaultDeserializer = defaultDeserializer;
        }
    
        @Override
        public void resolve(DeserializationContext ctxt) throws JsonMappingException {
            if (defaultDeserializer instanceof ResolvableDeserializer) {
                // We need to resolve the default deserializer, or else it won't work properly.
                ((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
            }
        }
    
        @Override
        public Result deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
            // let defaultDeserializer do the work:
            Result result = (Result) defaultDeserializer.deserialize(p, ctxt);
    
            // here you do your custom logic:
            String positionText = result.getPositionText();
            if (positionText != null) {
                try {
                    result.setPositionNumber(Integer.valueOf(positionText));
                } catch(NumberFormatException e) {
                    // positionText is not a valid integer
                }
            }
    
            return result;
        }
    }
    

    Finally you need to tell Jackson that you want the above ResultDeserializer to be used for deserializing Result objects. This is done by the following customization of the ObjectMapper, which will wrap your ResultDeserializer around Jackson's default deserializer, only if a Result object is to be deserialized:

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new SimpleModule()
                .setDeserializerModifier(new BeanDeserializerModifier() {
                
                    @Override
                    public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
                            BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
                        if (Result.class == beanDesc.getBeanClass())
                            return new ResultDeserializer(deserializer);  // your deserializer
                        return deserializer;
                    }
                }));
    

    Then you can deserialize your JSON content as usual, for example:

    File file = new File("example.json");
    List<Result> results = objectMapper.readValue(file, new TypeReference<List<Result>>() {});