3

I'm trying to deserialize json into object. However, the json has duplicate keys. I cannot change the json and I would like to use Jackson to change duplicate keys to a list.

Here is an example of the json I retrieve:

{
  "myObject": {
    "foo": "bar1",
    "foo": "bar2"
  }
}

And here is what I would like after deserialization:

{
  "myObject": {
    "foo": ["bar1","bar2"]
  }
}

I created my class like so:

public class MyObject {
    private List<String> foo;
    // constructor, getter and setter
}

I tried to use DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY from objectMapper but all it does is taking the last key and add it to the list like this:

{
  "myObject": {
    "foo": ["bar2"]
  }
}

Here is my objectMapperconfiguration:

new ObjectMapper().configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);

Is there a way to deserialize duplicate keys to a list using Jackson?

JsonMraz
  • 109
  • 2
  • 10
  • I don't think it's possible with `ObjectMapper` that's not a valid json. You'll have to use `JsonParser` – Oleg Apr 30 '20 at 17:46
  • There was a [similar question](https://stackoverflow.com/questions/42573120/parsing-a-json-which-contains-duplicate-keys) where accepted answer [suggests to use `net.sf.json.JSONObject`](https://stackoverflow.com/a/42574594/13279831) – Nowhere Man Apr 30 '20 at 18:38
  • https://stackoverflow.com/a/48424996/3152549 should help! – Kimchy Apr 30 '20 at 19:02
  • @AlexRudenko I prefer not to add another dependency but it looks like another solution. – JsonMraz May 01 '20 at 07:56
  • @Oleg I can try `JsonParser` but I think I would have to create a custom deserializer for that. If it's possible, I would like to find a solution without doing that. – JsonMraz May 01 '20 at 08:02
  • I tried this approach but I observe that my `@JsonAnySetter` annotated method does not get the duplicate entries when I Sys.out. I mean when I print the key and value within the `@JsonAnySetter` annotated method then I see only one entry for even duplicated key with last value. It is as if the Jackson has already ignored the duplicate entries before reaching out to this method. I have posted my question here if you get. a chance please let me know what should I do. https://stackoverflow.com/questions/67413028/java-jackson-jsonanysetter-does-not-store-the-values-of-duplicate-key – BATMAN_2008 May 09 '21 at 10:50

2 Answers2

5

You need to use com.fasterxml.jackson.annotation.JsonAnySetter annotation:

import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class JsonPathApp {

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

        ObjectMapper mapper = new ObjectMapper();
        Root root = mapper.readValue(jsonFile, Root.class);
        root.getMyObject().getFoos().forEach(System.out::println);
    }
}

class Root {

    private MyObject myObject;

    // getters, setters, toString
}

class MyObject {

    private List<String> foos = new ArrayList<>();

    @JsonAnySetter
    public void manyFoos(String key, String value) {
        foos.add(value);
    }

    // getters, setters, toString
}

On Java side you have a list with values:

bar1
bar2
Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
  • 1
    It looks like what I was looking for but how do you prevent from having this error `com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of 'java.util.ArrayList' out of VALUE_STRING token`? I tried to add this `new ObjectMapper().configure(DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY, false)` but it doesn't seem to work – JsonMraz May 01 '20 at 07:49
  • 1
    Great reply, @MichałZiober! – Nowhere Man May 01 '20 at 08:56
  • 1
    @JsonMraz, this is what `@JsonAnySetter` is doing. Notice, property name is a `foos` not `foo`. So, by default `Jackson` does not touch it and all other not-mapped properties are put to `manyFoos` method. – Michał Ziober May 01 '20 at 09:08
  • 1
    @MichałZiober I'm sorry, I didn't notice the `foos` with an "s". I added it with the `@JsonAnySetter` and it works! Thanks a lot! – JsonMraz May 01 '20 at 11:57
  • I tried this approach but I observe that my `@JsonAnySetter` annotated method does not get the duplicate entries when I `Sys.out`. I mean when I print the `key` and `value` within the `@JsonAnySetter` annotated method then I see only one entry for even duplicated `key` with last value. It is as if the `Jackson` has already ignored the duplicate entries before reaching out to this method. I have posted my question here if you get. a chance please let me know what should I do. https://stackoverflow.com/questions/67413028/java-jackson-jsonanysetter-does-not-store-the-values-of-duplicate-key – BATMAN_2008 May 09 '21 at 10:41
0

Supplementing answer from @Michał Ziober: if you need to handle case of mixing single values and lists/maps:

{  
  "myObject": {
     "foo": "bar1",
     "foo": ["bar2", "bar3"],
     "foo": {"bar": "baz"}
  }
}

this can be done in the same manyFoos:

@SuppressWarnings("unchecked")
@JsonAnySetter
public void manyFoos(String key, Object value) {
    System.out.println("key = " + key + " -> " + value.getClass() + ": " + value);
    if (value instanceof String)
        foos.add((String)value);
    else if (value instanceof Collection<?>) {
        foos.addAll((Collection<? extends String>)value);
    }
    else if (value instanceof Map<?, ?>) {
        Map<? extends String, ? extends String> subFoo = (Map<? extends String, ? extends String>) value;
        subFoo.entrySet().forEach(e -> foos.add(e.getKey() + ":" + e.getValue()));  
    }
}

Then the result will be:

{
  "myObject" : {
    "foos" : [ "bar1", "bar2", "bar3", "bar:baz" ]
  }
}
Nowhere Man
  • 19,170
  • 9
  • 17
  • 42
  • I tried this approach but I observe that my `@JsonAnySetter` annotated method does not get the duplicate entries when I `Sys.out`. I mean when I print the `key` and `value` for duplicate entries then I see only the last value for the duplicated key. It is as if the `Jackson` has already ignored the duplicate entries before reaching out to this method. I have posted my question here if you get. a chance please let me know what should I do. https://stackoverflow.com/questions/67413028/java-jackson-jsonanysetter-does-not-store-the-values-of-duplicate-key – BATMAN_2008 May 09 '21 at 10:39