1

I am using the Jackson library to deserialize JSON. In the JSON I have a few custom fields whose values can be anything, so I am trying to use the @JsonAnySetter and @JsonAnyGetter to obtain the values. The fields and values within the JSON can be duplicated and I would like to obtain everything from the JSON and store it within the map. However, the direct Jackson derealization is storing the last value if there are duplicates in the key.

I have a large JSON file that has many events. I am reading the file event-by-event so that the whole JSON file is not stored within memory. After reading a single event, I check the type of event, based on which I assign it to a different POJO. Following is my sample JSON file consisting of 2 events.

[
  {
    "isA": "Type1",
    "name": "Test",
    "foo": "val1",
    "foo": "val2",
    "bar": "val3",
    "foo": {
      "myField": "Value1",
      "myField": "value2"
    }
  },
  {
    "isA": "Type2",
    "name": "Test1",
    "foo": "val1",
    "foo": "val2",
    "bar": "val3",
    "foo": {
      "myField": "Value1",
      "myField": "value2"
    }
  }
]

Following is the class that is used for deserialization: (I have many fields which are mapped directly during the deserialization and working correctly so omitted for simplicity). This is the class for the type1 event if it's type2

@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Type1
{
    private String name;
    private Map<String, Object> userExtensions;
   
    @JsonAnyGetter
    public Map<String, Object> getUserExtensions() {
        return userExtensions;
    }
    
    @JsonAnySetter
    public void setUserExtensions(String key, Object value) {
        System.out.println("KEY : " + key + " VALUES : " + values);
    }
    
}

As you can observe from above I have a Map field that is used to populate the extensions using the JsonAnySetter. I have a method which will be called for the fields which cannot be directly searlized by Jackson.

I tried setting the System.out within the @JsonAnySetter method and I observed that Jackson does not get the duplicate field within this method:

  @JsonAnySetter
  public void setUserExtensions(String key, Object value) {
    System.out.println(" Key : " + key + " Value : " + value);
  }

I get only the last fields in this. For the above mentioned JSON I get only the last foo which has the value:

"foo" :{
  "myField" : "Value1"
  "myField" : "value2"
}

The first 2 foo with val1 and val2 does not even print within this method.

Following is my Main method which is actually the culprit as I am using the objectMapper.treeToValue of Jackson which does not support the duplicate fields.

public class Main
{
  public static void main (String[]args)
  {
    //File where the JSON is stored
    InputStream jsonStream = Main.class.getClassLoader().getResourceAsStream("InputEPCISEvents.json");
    final JsonFactory jsonFactory = new JsonFactory();
    final JsonParser jsonParser = jsonFactory.createParser (jsonStream);
    jsonParser.setCodec (new ObjectMapper ());
    final ObjectMapper objectMapper = new ObjectMapper();
    
    // Loop until the end of the events file
    while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
        // Get the node
        final JsonNode jsonNode = jsonParser.readValueAsTree();
        // Get the eventType
        final String eventType = jsonNode.get("isA").toString().replaceAll("\"", "");
        
        switch (eventType) {
            case "Type1":
                final Type1 objInfo = objectMapper.treeToValue(jsonNode, Type1.class);
                break;
            case "Type2":
                final Type2 objInfo = objectMapper.treeToValue(jsonNode, Type2.class);
                break;
            default:
                System.out.println("None of the event Matches");
                break;
        }
        
    }
      
  }
}

I wanted to know how can I make Jackson read the duplicate key values so that I can handle them within the @JsonAnySetter method.

I wanted to know if there is a way to handle these scenarios directly using Jackson or I need to build my own custom deserialization. If yes, then how can I build one for only one field within the class?

PS: I tried many things available online but none worked hence posting the same. If found duplicate then I am really sorry.

I noticed that most of the code sample on Stack Overflow read JSON with the Jackson readValue method, but in my case I have a large JSON file that has many events so I am splitting them event by event and using the Jackson objectMapper.treeToValue method to map it to my class. Within this class, I try to map the fields of each event with the respective class fields and if no match found then I am expecting them to be populated using the @JsonAnySetter method.

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
BATMAN_2008
  • 2,788
  • 3
  • 31
  • 98
  • I am still looking for an answer. I found that my `@JsonAnySetter` does not work due to the fact that I am using the `Jackson-objectMapper.treeToValue`. Can someone please provide me some sort of workaround to make it work with the `Jackson-objectMapper.treeToValue` instead of `Jackson-readValue` as most of the answers here are based on that. – BATMAN_2008 May 10 '21 at 04:32
  • 1
    [RFC-7159](https://datatracker.ietf.org/doc/html/rfc7159) specifies that keys in a JSON object should be unique, and _"When the names within an object are not unique, the behavior of software that receives such an object is unpredictable. Many implementations report the last name/value pair only. [...]"_ – Mark Rotteveel May 10 '21 at 09:59
  • @MarkRotteveel Thanks for editing the question and your response. Yes, I do agree that the `keys` in the `JSON` have to be unique. But it's recommended to have unique however JSON allows the duplicate key values. Moreover, I am receiving the input from other standard application which is out of my control to make modification and avoid getting the duplicate values. Also, `Jackson` does read the duplicate values when used with `ObjectMapper.readValue` and `@JsonAnySetter` method. So I would like to use the same approach to read the duplicate values by using the `ObjectMapper.treeToValue` method – BATMAN_2008 May 10 '21 at 10:04
  • @MarkRotteveel If you have any workaround or some approach that I can try to achieve the duplicate value reading using `Jackson` or `Custom` class then please let me know. – BATMAN_2008 May 10 '21 at 10:05
  • 1
    Can you post the final data structure you believe the resulting Events should be stored into? i.e. If you could tokenize and properly read this file... what would the resulting final POJO look like for each (or at least one) of your types of records? – Atmas May 10 '21 at 16:44
  • @Atmas Thanks for your response. I would like to store the information within the `Map>` basically I would check if the key exists within the method `setUserExtensions(String key, Object value)` if so then I will add it to the existing list if not create a new list. But the problem is that I am not even getting the duplicate key within the `setUserExtensions method System. out`. If I try to `Tokenize` the reading of the JSON file then I have to handle everything right? Like i need to check the field individually and assign it to the respective field within my POJO class. – BATMAN_2008 May 11 '21 at 03:51
  • All the events within my actual JSON file are present within the `EventList` array and before this array, I have few headers which I need to skip for reaching the `EventList` for that reason I am actually using the `JsonParser` so that I can traverse through the file and reach the `EventList` after reaching to the `EventList` I am using the `JsonNode` and `treeToValue` to map the whole event one-by-one to my POJO class. I hope I have provided the answer which you were looking for. – BATMAN_2008 May 11 '21 at 03:54
  • I think I understand now. You are trying to have a behavior similar to how http query GET params are processed. As when multiple params are in the url it's treated as a list. Will have to think on it some more. However since you have map with string key at the top then the top level data structure must have a unique ID in the key of that map. What is this unique ID supposed to be? – Atmas May 11 '21 at 14:01
  • Also there is ALREADY a data structure in JSON that represents lists/arrays which I'm sure you must know of.. so it will understandably be an uphill battle for you to break JSON in a way that reinterprets lists in your representation of a list. Is there no way for you to change the provider of the data to consider putting it into a different or standard form? – Atmas May 11 '21 at 14:04
  • @Atmas Yes, you are correct. I am planning to have `Map>` as and when I get a `key and value` within the `setUserExtensions(String key, Object value)` I will check if the element with that `key` is already present if so I will add to existing `List` if not then I will create a new entry within the `MAP`. My keys would be the keys from JSON such as `foo,bar` etc. I hope I have provided you with the right explanation. But the problem that I am facing is that the duplicate keys do not even come within that method. If the key is duplicate then I get only the last entry – BATMAN_2008 May 11 '21 at 14:07
  • 1
    Oh I'm sorry. You said that and I think I missed that you even wanted that at the top level. Yes I see that should technically make sense and I'll think on it. Hopefully others can see your goal too and consider it as well. – Atmas May 11 '21 at 14:09
  • @Atmas I am glad you were able to understand the issue. Even I am trying different things if something works out then I will inform you here. If something comes to your mind then please do post or if you need any further clarification then I am happy to provide it. With regards to your last question: the JSON data is coming from a different source. I have no way to change it. However, before assigning it to my Object class I can manipulate it if that's the last option we have. But I would really happy if there is a JACKSON way of doing it similar in the `Jackson readValue` – BATMAN_2008 May 11 '21 at 14:13

2 Answers2

2

I think I have a solution that may work for you, or at the very least move you a few steps forward. I created a local test using your files to demonstrate.

I figured out a way to ration about it by looking for how FAIL_ON_READING_DUP_TREE_KEY was implemented. Basically, the code that's responding to it is in JsonNodeDeserializer. If you see how this deserializer looks at _handleDuplicateField, it will basically just replace the newest node with the latest (the side effect you're seeing), and if you set the fail feature it will then conditionally throw an exception. However, by creating an extension of this deserializer class with an overriden method, we can basically implement the behavior of "merge the dupe" instead of "throw an error when you see one". The results of that are below.

The console logs from my test run yield this, demonstrating that "foos" contains all the independent strings and objects...

Since it did the merge at the JSON level instead of at the object level, it seems to have even inadvertently handled the inner myField use case by merging the multiple strings into a single arrayNode... I wasn't even trying to solve for myField at the time, but hey, there you go too!

I didn't handle Type2, because I figure this is enough to get you going.

Of course, you will have to create the file with your contents above in the test package as jsonmerge/jsonmerge.json

Console log results

Object Node Read with Dupes: {"isA":"Type1","name":"Test","foo":["val1","val2",{"myField":["Value1","value2"]}],"bar":"val3"}
Type 1 mapped after merge : Type1 [isA=Type1, name=Test, bar=val3, foos=[val1, val2, {myField=[Value1, value2]}]]
None of the event Matches
EOF

Test main code - JsonMerger.java

package jsonmerge;

import java.io.InputStream;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.ObjectNode;

/**
 * Project to resolve https://stackoverflow.com/questions/67413028/jackson-jsonanysetter-ignores-values-of-duplicate-key-when-used-with-jackson-ob
 *
 */
public class JsonMerger {

    public static void main(String[] args) throws Exception {

        // File where the JSON is stored
        InputStream jsonStream = JsonMerger.class.getClassLoader()
            .getResourceAsStream("jsonmerge/jsonmerge.json");
        final JsonFactory jsonFactory = new JsonFactory();
        final ObjectMapper objectMapper = new ObjectMapper();

        // Inject a deserializer that can handle/merge duplicate field declarations of whatever object(s) type into an arrayNode with those objects inside it
        SimpleModule module = new SimpleModule();
        module.addDeserializer(JsonNode.class, new JsonNodeDupeFieldHandlingDeserializer());
        objectMapper.registerModule(module);

        final JsonParser jsonParser = jsonFactory.createParser(jsonStream);
        jsonParser.setCodec(objectMapper);

        JsonToken nextToken = jsonParser.nextToken();

        // Loop until the end of the events file
        while (nextToken != JsonToken.END_ARRAY) {
            nextToken = jsonParser.nextToken();

            final JsonNode jsonNode = jsonParser.readValueAsTree();
            if (jsonNode == null || jsonNode.isNull()) {
                System.out.println("EOF");
                break;
            }

            // Get the eventType
            JsonNode getNode = jsonNode.get("isA");
            final String eventType = getNode
                .toString()
                .replaceAll("\"", "");

            switch (eventType) {
            case "Type1":
                final Object obj = objectMapper.treeToValue(jsonNode, JsonNodeDupeFieldHandlingDeserializer.class);
                ObjectNode objNode = (ObjectNode) obj;
                System.out.println();
                System.out.println();

                System.out.println("Object Node Read with Dupes: " + objNode);

                Type1 type1 = objectMapper.treeToValue(objNode, Type1.class);
                System.out.println("Type 1 mapped after merge : " + type1);

                break;
            default:
                System.out.println("None of the event Matches");
                break;
            }

        }

    }
}

Type1.java

package jsonmerge;

import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonAnySetter;

public class Type1 {

    public String       isA;
    public String       name;
    public String       bar;
    public List<Object> foos;

    public Type1() {

        foos = new LinkedList<Object>();
    }

    /**
     * Inspired by https://stackoverflow.com/questions/61528937/deserialize-duplicate-keys-to-list-using-jackson
     * 
     * @param key
     * @param value
     */
    @JsonAnySetter
    public void fieldSetters(String key, Object value) {

        // System.out.println("key = " + key + " -> " + value.getClass() + ": " + value);
        // Handle duplicate "foo" fields to merge in strings/objects into a List<Object>
        if ("foo".equals(key) && value instanceof String) {
            foos.add((String) value);
        } else if ("foo".equals(key) && (value instanceof Collection<?>)) {
            foos.addAll((Collection<?>) value);
        }

    }

    @Override
    public String toString() {

        return "Type1 [isA=" + isA + ", name=" + name + ", bar=" + bar + ", foos=" + foos + "]";
    }

}

JsonNodeDupeFieldHandlingDeserializer

package jsonmerge;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.std.JsonNodeDeserializer;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;

/**
 * Handles JsonNodes by merging their contents if their values conflict into an arrayNode
 * 
 */
@JsonDeserialize(using = JsonNodeDupeFieldHandlingDeserializer.class)
public class JsonNodeDupeFieldHandlingDeserializer extends JsonNodeDeserializer {

    private static final long serialVersionUID = 1L;

    @Override
    protected void _handleDuplicateField(JsonParser p, DeserializationContext ctxt, JsonNodeFactory nodeFactory, String fieldName, ObjectNode objectNode, JsonNode oldValue, JsonNode newValue) throws JsonProcessingException {

        // When you encounter a duplicate field, instead of throwing an exception in this super-logic... (if the feature was set anyway)
//      super._handleDuplicateField(p, ctxt, nodeFactory, fieldName, objectNode, oldValue, newValue);

        // Merge the results into a common arrayNode
        // Note, this assumes that multiple values will combine into an array...
        // And *THAT* array will be used for future nodes to be tacked onto...
        // But if the FIRST thing to combine *IS* an array, then it will be the
        // initial array...
        // You could probably persist some way to track whether the initial array was encountered, but don't want to think about that now..
        ArrayNode asArrayValue = null;

        if (oldValue.isArray()) {
            asArrayValue = (ArrayNode) oldValue;
        } else {
            // If not, create as array for replacement and add initial value..
            asArrayValue = nodeFactory.arrayNode();
            asArrayValue.add(oldValue);
        }

        asArrayValue.add(newValue);
        objectNode.set(fieldName, asArrayValue);

    }

}
Atmas
  • 2,389
  • 5
  • 13
  • Thanks a lot for your efforts. I really appreciate your time and dedication. I will try this approach and inform you if this is working as expected. – BATMAN_2008 May 11 '21 at 17:22
  • This is working as expected based on the sample code I have provided in the question. I am trying to adapt the same to my actual application code so I am making few changes accordingly. Thanks a lot again for the effort and answer. You have earned bounty and the correct answer. I will make the modifications and if I have some doubt then I will reach out to your again. Have a nice day ahead :) – BATMAN_2008 May 11 '21 at 18:04
  • 1
    Couldn't have helped a kinder member of the community! Thanks for the award and I would be happy to ponder on this some more if you run into a new conundrum. I'll keep the test project handy just in case. :-) – Atmas May 11 '21 at 18:07
-1

With this test data:

{
  "foo" : "val1",
  "foo" : "val2",
  "bar" : "val3",
  "foo" :{
    "myField" : "Value1",
    "myField" : "value2"
  }
}

This code:

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

import java.io.File;

public class Main2 {

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

        ObjectMapper mapper = new ObjectMapper();
        MyObject myObject = mapper.readValue(jsonFile, MyObject.class);
    }
}

class MyObject {
    // attributes

    @JsonAnySetter
    public void manyFoos(String key, Object value) {
        System.out.println(" Key : " + key + " Value : " + value);
    }
}

Prints:

Key : foo Value : val1
Key : foo Value : val2
Key : bar Value : val3
Key : foo Value : {myField=value2}

The problem is that you have duplicate keys on multiple levels. With this code you can handle the duplicates on the outer level, and do as you want, but jackson deserializes the value part. In case of the third foo, the value is a map, and because of that, duplicates are lost.

Tamás Pollák
  • 233
  • 1
  • 8
  • Thanks for taking your time and responding. I have tried this approach actually in my updated code I have provided the same thing with the method `setUserExtensions ` in this it's providing only the last `key` with its `value` due to which I am losing the previous duplicated `key-value`. – BATMAN_2008 May 09 '21 at 16:44
  • One more thing I noticed is that most of the code sample here including yours read the JSON with `Jackson-readValue` but in my case I have a large JSON file which has many events so I am spliting them event by event and using the `Jackson-objectMapper.treeToValue` method to map it to my class. Could that be issue due to which I am missing out on the old values for the duplicate keys? – BATMAN_2008 May 09 '21 at 16:47
  • 1
    @BATMAN_2008, `treeToValue` method is only a part of the problem. To invoke it, you need to provide `TreeNode` which in case of `JSON Object` is `ObjectNode`. This class does not support duplicated fields and information about duplicated fields were lost when you deserialised `JSON` to `TreeNode`. You have only one way to fix it, is to deserialise `JSON` payload directly to `POJO`. – Michał Ziober May 09 '21 at 19:59
  • @MichałZiober Thanks a lot for the answer. Actually, I have a large JSON file that has many events. I have to check incoming event is of which type and based on it I have to map it to a different `POJO`. Since the file is large I do not want to store the whole file in memory hence I am reading the file event-by-event and assigning it to a different `POJO`. Hence, I am using the method `objectMapper.treeToValue`. So I don't think it would be possible for me to assign the whole `JSON` to a particular `POJO`. Is there any other workaround that I can try to resolve this? – BATMAN_2008 May 10 '21 at 03:43
  • @MichałZiober I have edited my question so that it can be useful for someone who is reading and also they do not end-up providing the answer which would work for `mapper.readValue` and not for `mapper.treeToValue`. If you get a chance please have a look and provide me with some sort of workaround on how can I handle it and obtain the duplicate fields in my `@JsonAnySetter` method without hampering the reading `JSON` file as I would like to continue to use the `Jackson` to read the file event-by-event method – BATMAN_2008 May 10 '21 at 04:12
  • @MichałZiober I tried a lot of things but still no luck accessing all the fields from JSON. As the fields are user-generated fields these fields can be random so there is no way for me to predict what kind of `key-value` will be present so its not possible for me to map it directly to my field in `POJO`. Hence, I was using the `@JsonAnySetter` method to populate my `Map` then process them according to my need. Can you please help me in accessing the duplicate fields from the `JSON` while using the `ObjectMapper-treeToValue`? Or if you can guide me through a basic explantion. – BATMAN_2008 May 10 '21 at 07:33