58

Is it possible to merge two JSON documents with the Jackson JSON library? I am basically using the Jackson mapper with simple Java Maps.

I've tried to search in Google and Jackson's documentation but couldn't find anything.

Danish
  • 3,708
  • 5
  • 29
  • 48

8 Answers8

75

Inspired by StaxMans answer I implemented this merging method.

public static JsonNode merge(JsonNode mainNode, JsonNode updateNode) {

    Iterator<String> fieldNames = updateNode.fieldNames();
    while (fieldNames.hasNext()) {

        String fieldName = fieldNames.next();
        JsonNode jsonNode = mainNode.get(fieldName);
        // if field exists and is an embedded object
        if (jsonNode != null && jsonNode.isObject()) {
            merge(jsonNode, updateNode.get(fieldName));
        }
        else {
            if (mainNode instanceof ObjectNode) {
                // Overwrite field
                JsonNode value = updateNode.get(fieldName);
                ((ObjectNode) mainNode).put(fieldName, value);
            }
        }

    }

    return mainNode;
}

Hope this helps someone.

c4k
  • 4,270
  • 4
  • 40
  • 65
Arne
  • 1,364
  • 1
  • 11
  • 22
  • Looks pretty good @Arn! It will be helpful if I need to do this in a future project. When I asked this question, I was working with elasticsearch and I found a partial update plugin for it. That helped me at that time. – Danish Jul 13 '12 at 16:26
  • 3
    Excellent, Arne, that is exactly what I've needed, you've saved me hours of investigation, thank you. To make this a little more than `thank you` comment, as of Jackson 2.4 `put()` is deprecated and should be substituted with `replace()`. Also, for JDK8 users, the code could be made more concise with trivial invoking of `forEachRemaining()` directly on the iterator and passing in the somewhat shorter lambda-expression. – quantum Jun 18 '15 at 22:58
  • 1
    great thanks. does this work for embedded arrays? both when adding elements to the array and when removing elements ? or do we have to replace the whole array? – abdel Mar 13 '19 at 10:37
64

One way is to use ObjectReader like so:

MyBean defaults = objectMapper.readValue(defaultJson, MyBean.class);
ObjectReader updater = objectMapper.readerForUpdating(defaults);
MyBean merged = updater.readValue(overridesJson);

which will combine data from two sources. This only makes a shallow copy, i.e. does not do recursive merge on contained objects.

Otherwise you may need to just read JSON as a tree (JsonNode), loop over contents and merge manually. This often makes sense anyway since rules of merging are not trivial, and everyone has their own ideas of how merging should work.

EDIT: (03-Apr-2017)

As per @Fernando Correia's comment, there is actually a new feature added in upcoming Jackson 2.9 (to be released in April or May 2017) that does allow deep merging, finally.

StaxMan
  • 113,358
  • 34
  • 211
  • 239
  • 1
    Thank you for your answer. I do need a deep merge, hence you're suggestion to write the merge manually echos the gut feeling I had when I posted the question. My underlying data structures are Maps, hence it should be worth writing a generic deep merge routine for java Maps. It may prove to be re-usable in some future project. – Danish Mar 29 '12 at 18:01
  • is deep merge available as a feature now ? – yathirigan Aug 19 '15 at 05:37
  • No, not as of Jackson 2.6. – StaxMan Sep 10 '15 at 17:48
  • 3
    It is implemented for the (upcoming) version 2.9: https://github.com/FasterXML/jackson-databind/issues/1399 – Fernando Correia Mar 27 '17 at 23:44
  • 1
    @FernandoCorreia well spotted, it is indeed! Thank you for adding an update; I will add an edit to my answer. – StaxMan Apr 03 '17 at 21:15
20

Inspired by Arn's answer. Editing it to add the case where a node may have a array of nodes in it.

public static JsonNode merge(JsonNode mainNode, JsonNode updateNode) {

    Iterator<String> fieldNames = updateNode.fieldNames();

    while (fieldNames.hasNext()) {
        String updatedFieldName = fieldNames.next();
        JsonNode valueToBeUpdated = mainNode.get(updatedFieldName);
        JsonNode updatedValue = updateNode.get(updatedFieldName);

        // If the node is an @ArrayNode
        if (valueToBeUpdated != null && valueToBeUpdated.isArray() && 
            updatedValue.isArray()) {
            // running a loop for all elements of the updated ArrayNode
            for (int i = 0; i < updatedValue.size(); i++) {
                JsonNode updatedChildNode = updatedValue.get(i);
                // Create a new Node in the node that should be updated, if there was no corresponding node in it
                // Use-case - where the updateNode will have a new element in its Array
                if (valueToBeUpdated.size() <= i) {
                    ((ArrayNode) valueToBeUpdated).add(updatedChildNode);
                }
                // getting reference for the node to be updated
                JsonNode childNodeToBeUpdated = valueToBeUpdated.get(i);
                merge(childNodeToBeUpdated, updatedChildNode);
            }
        // if the Node is an @ObjectNode
        } else if (valueToBeUpdated != null && valueToBeUpdated.isObject()) {
            merge(valueToBeUpdated, updatedValue);
        } else {
            if (mainNode instanceof ObjectNode) {
                ((ObjectNode) mainNode).replace(updatedFieldName, updatedValue);
            }
        }
    }
    return mainNode;
}
Jay Khatwani
  • 389
  • 4
  • 6
  • The casting of `valueToBeUpdated` to an `ArrayNode` is not safe. You can get a `TextNode`, for instance, instead of an `ArrayNode` in that variable. – PNS Oct 28 '16 at 20:36
  • Thanks PNS. I have edited my answer and added the condition to check if valueToBeUpdated is an arrayNode – Jay Khatwani Nov 09 '16 at 23:52
  • 1
    Cool - but doesn't work if updateNode starts with an ArrayNode, because they do not have fields, just children. BTW - ObjectReader updating work for me. – Paul Bartlett Sep 29 '18 at 19:52
  • 1
    Thanks to you I'm going to sleep more hour today, thank you my friend!! :) – Alexis Gamarra Apr 09 '19 at 04:03
  • Thanks a ton for this. Works like a charm. Btw it doesn't work if root node is JSONArray, so I just modified the code by copying while-loop's ArrayNode code at top level in if-else – VaibS Sep 02 '22 at 10:54
6

Below is an implementation in Scala. The source and target node are mostly commutative except when a branch exists in both source and target.

  def mergeYamlObjects(source: ObjectNode, target: ObjectNode, overwrite: Boolean = true): ObjectNode = {
    if (target == null)
      source
    else if (source == null)
      target
    else {
      val result = source.deepCopy
      val fieldlist = source.fieldNames.asScala.toList ++ target.fieldNames.asScala.toList
      for (item <- fieldlist) {
        if (!(source has item)) {
          result put(item, target get item)
        } else {
          if ((source get item).isValueNode) {
            if (target has item)
              if (overwrite)
                result.put(item, target get item)
          } else {
            result.put(item, mergeYamlObjects(source.get(item).asInstanceOf[ObjectNode],
              target.get(item).asInstanceOf[ObjectNode], overwrite = overwrite))
          }
        }
      }
      result
    }
  }
rogue-one
  • 11,259
  • 7
  • 53
  • 75
6

If someone simply wants to add two or more JsonNode object into one JsonNode, this can be one approach:

ArrayNode arrayNode = objectMapper.createArrayNode();
arrayNode.add(firstJsonNode);
arrayNode.add(secondJsonNode);
arrayNode.add(thirdJsonNode);

JsonNode root = JsonNodeFactory.instance.objectNode();
((ObjectNode) root).put("", arrayNode);
System.out.println("merged array node #: " + root);
potame
  • 7,597
  • 4
  • 26
  • 33
  • 1
    The method put(String, JsonNode) from the type ObjectNode is deprecated , use set or replace – emecas Apr 07 '23 at 20:25
3

Here,is full implementation of merging two JSON tree into one. Hope it would be helpful :)

/**
 * Merge two JSON tree into one i.e mergedInTo.
 *
 * @param toBeMerged
 * @param mergedInTo
 */
public static void merge(JsonNode toBeMerged, JsonNode mergedInTo) {
    Iterator<Map.Entry<String, JsonNode>> incomingFieldsIterator = toBeMerged.fields();
    Iterator<Map.Entry<String, JsonNode>> mergedIterator = mergedInTo.fields();

    while (incomingFieldsIterator.hasNext()) {
        Map.Entry<String, JsonNode> incomingEntry = incomingFieldsIterator.next();

        JsonNode subNode = incomingEntry.getValue();

        if (subNode.getNodeType().equals(JsonNodeType.OBJECT)) {
            boolean isNewBlock = true;
            mergedIterator = mergedInTo.fields();
            while (mergedIterator.hasNext()) {
                Map.Entry<String, JsonNode> entry = mergedIterator.next();
                if (entry.getKey().equals(incomingEntry.getKey())) {
                    merge(incomingEntry.getValue(), entry.getValue());
                    isNewBlock = false;
                }
            }
            if (isNewBlock) {
                ((ObjectNode) mergedInTo).replace(incomingEntry.getKey(), incomingEntry.getValue());
            }
        } else if (subNode.getNodeType().equals(JsonNodeType.ARRAY)) {
            boolean newEntry = true;
            mergedIterator = mergedInTo.fields();
            while (mergedIterator.hasNext()) {
                Map.Entry<String, JsonNode> entry = mergedIterator.next();
                if (entry.getKey().equals(incomingEntry.getKey())) {
                    updateArray(incomingEntry.getValue(), entry);
                    newEntry = false;
                }
            }
            if (newEntry) {
                ((ObjectNode) mergedInTo).replace(incomingEntry.getKey(), incomingEntry.getValue());
            }
        }
        ValueNode valueNode = null;
        JsonNode incomingValueNode = incomingEntry.getValue();
        switch (subNode.getNodeType()) {
            case STRING:
                valueNode = new TextNode(incomingValueNode.textValue());
                break;
            case NUMBER:
                valueNode = new IntNode(incomingValueNode.intValue());
                break;
            case BOOLEAN:
                valueNode = BooleanNode.valueOf(incomingValueNode.booleanValue());
        }
        if (valueNode != null) {
            updateObject(mergedInTo, valueNode, incomingEntry);
        }
    }
}

private static void updateArray(JsonNode valueToBePlaced, Map.Entry<String, JsonNode> toBeMerged) {
    toBeMerged.setValue(valueToBePlaced);
}

private static void updateObject(JsonNode mergeInTo, ValueNode valueToBePlaced,
                                 Map.Entry<String, JsonNode> toBeMerged) {
    boolean newEntry = true;
    Iterator<Map.Entry<String, JsonNode>> mergedIterator = mergeInTo.fields();
    while (mergedIterator.hasNext()) {
        Map.Entry<String, JsonNode> entry = mergedIterator.next();
        if (entry.getKey().equals(toBeMerged.getKey())) {
            newEntry = false;
            entry.setValue(valueToBePlaced);
        }
    }
    if (newEntry) {
        ((ObjectNode) mergeInTo).replace(toBeMerged.getKey(), toBeMerged.getValue());
    }
}
Ajay Kumar
  • 4,864
  • 1
  • 41
  • 44
0

If your goal is to concatenate two JSONs, the simplest way I found is the following (considering you have two ObjecNode ready):

ObjectMapper mapper = new ObjectMapper();

// Dummy objects to concatenate
Map<String, Object> map = new HashMap();
map.put("k", "v");

Map<String, Object> secondMap = new HashMap();
secondMap.put("secondK", "secondV");

//Transforming Objects into ObjectNode
ObjectNode firstObjectNode = mapper.convertValue(map, ObjectNode.class);
ObjectNode secondObjectNode = mapper.convertValue(secondMap, ObjectNode.class);

//Concatenating secondObjectNode into firstObjectNode
firstObjectNode.setAll(secondObjectNode);

//Output will be: {"k":"v","secondK":"secondV"}
System.out.println(firstObjectNode);

Hope it helps

Evandro
  • 137
  • 1
  • 1
  • 13
0

Merge json -> json with ObjectMapper:

@SneakyThrows
public static String mergeToPretty(String baseJson, String overrideJson) {
    ObjectMapper objectMapper = new ObjectMapper();
    
    Object base = objectMapper.readValue(baseJson, Object.class);
    Object merged = objectMapper.readerForUpdating(base).readValue(overrideJson);
    return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(merged);
}

Notes:

  • @SneakyThrows could be replaced with exception handling if not using Lombok.
  • If no need for pretty, remove .writerWithDefaultPrettyPrinter() from last line.
Daniel Hári
  • 7,254
  • 5
  • 39
  • 54