12

Oftentimes, I have a need to merge two JSON Objects (similar to the way jQuery's $.extend() works). However, the Gson library has no built in functionality and they have said they won't implement it.

Doing something like:

private void merge(JsonObject firstObj, JsonObject secondObj){
    for(String keyInSecondObj : secondObj.entrySet().keySet()) {
      if(!firstObj.has(keyInSecondObj )){
        firstObj.add(secondMap.get(keyInSecondObj));
    }
}

Is too simple because it doesn't handle recursively merging JsonObjects, doesn't handle conflicts when the key exists in both maps, and has no special handling for non-primitive values such as Arrays.

I failed to find any pre-built solutions to do this. I would prefer to use something that has been thoroughly tested instead of writing my own method, but it must be Gson (not Jackson or other).

Edit: I ended up writing my own implementation as have added as an answer to this question

This question is not a duplicate because it's not using Gson (or Java for that matter).

Community
  • 1
  • 1
bradvido
  • 2,743
  • 7
  • 32
  • 49
  • 1
    This is simlar, but doesn't use the Gson library: http://stackoverflow.com/questions/21160337/how-can-i-merge-two-jobject – bradvido Dec 04 '15 at 16:08
  • Please read the tag summary before adding them to your question... – Sumurai8 Dec 04 '15 at 16:24
  • 1
    @JarrodRoberson This question is not a dupe! Please see my explanation. The marked duplicate is not even using Java – bradvido Dec 08 '15 at 21:08
  • this is way **off-topic** in almost every way, *no code*, *asking for recommendations*, *too broad* and *opinion based* all. As well as being a duplicate in the *most general sense of an approach* because of the previously mentioned reasons for being off-topic. With this poor of a question one off-topic reason is as much as good as another since we can't not nominate for multiple reasons. –  Dec 08 '15 at 21:26
  • 2
    @JarrodRoberson I have a link to *specific* Gson library information about this feature, and have my own code as a proposed solution to this question (to help the community). Is there a better way or should I refrain from trying to do this? – bradvido Dec 08 '15 at 21:31

4 Answers4

18

Here's my first attempt at writing my own static merge method. Feel free to poke holes in it.

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.Map;

public class GsonTools {

    public static enum ConflictStrategy {

        THROW_EXCEPTION, PREFER_FIRST_OBJ, PREFER_SECOND_OBJ, PREFER_NON_NULL;
    }

    public static class JsonObjectExtensionConflictException extends Exception {

        public JsonObjectExtensionConflictException(String message) {
            super(message);
        }

    }

    public static void extendJsonObject(JsonObject destinationObject, ConflictStrategy conflictResolutionStrategy, JsonObject ... objs) 
            throws JsonObjectExtensionConflictException {
        for (JsonObject obj : objs) {
            extendJsonObject(destinationObject, obj, conflictResolutionStrategy);
        }
    }

    private static void extendJsonObject(JsonObject leftObj, JsonObject rightObj, ConflictStrategy conflictStrategy) 
            throws JsonObjectExtensionConflictException {
        for (Map.Entry<String, JsonElement> rightEntry : rightObj.entrySet()) {
            String rightKey = rightEntry.getKey();
            JsonElement rightVal = rightEntry.getValue();
            if (leftObj.has(rightKey)) {
                //conflict                
                JsonElement leftVal = leftObj.get(rightKey);
                if (leftVal.isJsonArray() && rightVal.isJsonArray()) {
                    JsonArray leftArr = leftVal.getAsJsonArray();
                    JsonArray rightArr = rightVal.getAsJsonArray();
                    //concat the arrays -- there cannot be a conflict in an array, it's just a collection of stuff
                    for (int i = 0; i < rightArr.size(); i++) {
                        leftArr.add(rightArr.get(i));
                    }
                } else if (leftVal.isJsonObject() && rightVal.isJsonObject()) {
                    //recursive merging
                    extendJsonObject(leftVal.getAsJsonObject(), rightVal.getAsJsonObject(), conflictStrategy);
                } else {//not both arrays or objects, normal merge with conflict resolution
                    handleMergeConflict(rightKey, leftObj, leftVal, rightVal, conflictStrategy);
                }
            } else {//no conflict, add to the object
                leftObj.add(rightKey, rightVal);
            }
        }
    }

    private static void handleMergeConflict(String key, JsonObject leftObj, JsonElement leftVal, JsonElement rightVal, ConflictStrategy conflictStrategy) 
            throws JsonObjectExtensionConflictException {
        {
            switch (conflictStrategy) {
                case PREFER_FIRST_OBJ:
                    break;//do nothing, the right val gets thrown out
                case PREFER_SECOND_OBJ:
                    leftObj.add(key, rightVal);//right side auto-wins, replace left val with its val
                    break;
                case PREFER_NON_NULL:
                    //check if right side is not null, and left side is null, in which case we use the right val
                    if (leftVal.isJsonNull() && !rightVal.isJsonNull()) {
                        leftObj.add(key, rightVal);
                    }//else do nothing since either the left value is non-null or the right value is null
                    break;
                case THROW_EXCEPTION:
                    throw new JsonObjectExtensionConflictException("Key " + key + " exists in both objects and the conflict resolution strategy is " + conflictStrategy);
                default:
                    throw new UnsupportedOperationException("The conflict strategy " + conflictStrategy + " is unknown and cannot be processed");
            }
        }
    }
}
bradvido
  • 2,743
  • 7
  • 32
  • 49
  • 1
    I've voted to reopen your original question. https://stackoverflow.com/questions/34092373/merge-extend-json-objects-using-gson-in-java I'm facing a similar problem and have chosen to use https://github.com/fge/json-patch/blob/master/README.md#json-diff – fadedbee Mar 03 '16 at 09:21
  • 1
    Thanks for this implementation! This looks great! will try it out and see if it works for my needs. – Yogesh Nov 26 '18 at 20:50
7

You can use

  Map firstObject = new GSON().fromJson(json1, HashMap.class);
  Map secondObject = new GSON().fromJson(json2, HashMap.class);

// merge Map firstObject and secondObject as you want, see this post

  String resultJson = new GSON().toJson(resultMap); 
Community
  • 1
  • 1
Slava Vedenin
  • 58,326
  • 13
  • 40
  • 59
  • 2
    This strategy is naive in that is just adds any values that exist in the second map to the first map. It doesn't handle what to do if there are conflicts (exist in both objects), or if the values are non-primitive (recursive object merging & array concatenation) – bradvido Dec 08 '15 at 21:38
  • 2
    Yes, but you can merge conflit by hand, using just merge two HashMap – Slava Vedenin Dec 08 '15 at 21:39
  • 2
    by hand!? really? – bradvido Aug 25 '16 at 21:19
2

Library class below allows deep merge of arrays and objects. They apply the strategy as described, but you can adopt your own strategy by changing simple operations in the middle of methods.

The "Overlay" stratagy of merging (overwrite or add):

  • Primitive type fields are overwritten.
  • Objects with equal declared keys are combined.
  • Objects without declared keys are combined if object contents are equal.
  • Other objects are overwritten or added.
  • Arrays are combined: duplicate items are added in the same array until the amount of such items is equal in both arrays.

Usage (String or Gson JsonObject/JsonArray can be returned):

// Straightforward object merging:

Json.mergeObjects(    
  "{my_object_as_string}",
  "{my_other_object_as_string}");

// Merge "my_objects" arrays and set object identity keys:

HashMap<String, String[]> keyCombinations = new HashMap<>();
  keyCombinations.put(
    "objects",
    new String[] {"object_identity_key_one", "object_identity_key_two"});

Json.mergeArrays(
  "my_objects",
  keyCombinations,
  "[my_array_as_string]",
  "[my_other_array_as_string]"));

The Library Class:

package com.example.utils;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

import java.util.ArrayList;
import java.util.HashMap;

import io.reactivex.annotations.NonNull;

@SuppressWarnings("unused")
public class Json {

  /**
   * Merge given JSON-objects. Same keys are merged for objects and 
   * overwritten by last object for primitive types.
   *
   * @param keyCombinations Key names for unique object identification. 
   *                        Or empty collection.
   * @param objects Any amount of JSON-objects to merge.
   *
   * @return Merged JSON-object.
   */
  public static JsonObject mergeObjects(
    @NonNull
      HashMap<String, String[]> keyCombinations,
    Object... objects) {

    JsonObject mergedObject = new JsonObject();

    for (Object object : objects) {

      JsonObject jsonObject = (JsonObject) object;

      for (String key : jsonObject.keySet()) {

        JsonElement parameter = jsonObject.get(key);

        if (mergedObject.has(key)) {

          // Key name matches:

          if (jsonObject.get(key).isJsonObject()) {

            // This is object - merge:

            parameter =
              mergeObjects(
                keyCombinations,
                mergedObject.get(key).getAsJsonObject(),
                jsonObject.get(key).getAsJsonObject());

          } else if (jsonObject.get(key).isJsonArray()) {

            // This is array - merge:

            parameter =
              mergeArrays(
                key,
                keyCombinations,
                mergedObject.get(key).getAsJsonArray(),
                jsonObject.get(key).getAsJsonArray());

          } else {

            // This is neither object nor array - replace value:

            mergedObject.add(key, parameter);
          }
        }

        // No such field yet - add:

        mergedObject.add(key, parameter);
      }
    }

    return mergedObject;
  }

  /**
   * Alternative - no object identity keys are set.
   * See {@link Json#mergeObjects(HashMap, Object...)}
   */
  public static JsonObject mergeObjects(
    Object... objects) {

    return (
      mergeObjects(
        new HashMap<>(),
        objects));
  }

  /**
   * Get GSON-object from string.
   *
   * @param jsonString JSON-object as string.
   *
   * @return JsonObject (GSON).
   */
  public static JsonObject getJsonObject(String jsonString) {

    JsonObject jsonObject = new JsonObject();
    JsonParser parser;

    parser = new JsonParser();

    if (jsonString != null) {

      jsonObject =
        parser
          .parse(
            jsonString)
          .getAsJsonObject();
    }

    return jsonObject;
  }

  /**
   * See {@link Json#mergeObjects(HashMap, Object...)}
   */
  public static String mergeObjects(
    HashMap<String, String[]> keyCombinations,
    String... jsonObjects) {

    ArrayList<JsonObject> objects = new ArrayList<>();

    for (String jsonObject : jsonObjects) {

      objects.add(
        Json2.getJsonObject(jsonObject));
    }

    return (
      mergeObjects(
        keyCombinations,
        objects.toArray())
        .toString());
  }

  /**
   * Alternative - no object identity keys are set.
   * See {@link Json#mergeObjects(HashMap, Object...)}
   */
  public static String mergeObjects(
    String... jsonObjects) {

    ArrayList<JsonObject> objects = new ArrayList<>();

    for (String jsonObject : jsonObjects) {

      objects.add(
        getJsonObject(jsonObject));
    }

    return (
      mergeObjects(
        new HashMap<>(),
        objects.toArray())
        .toString());
  }

  /**
   * See {@link Json#mergeArrays(String, HashMap, Object...)}
   */
  public static String mergeArrays(
    String arrayName,
    HashMap<String, String[]> keyCombinations,
    String... jsonArrays) {

    ArrayList<JsonArray> arrays = new ArrayList<>();

    for (String jsonArray : jsonArrays) {

      arrays.add(
        getJsonArray(jsonArray));
    }

    return (
      mergeArrays(
        arrayName,
        keyCombinations,
        arrays.toArray())
        .toString());
  }

  /**
   * Alternative - no object identity keys are set.
   * See {@link Json#mergeArrays(String, HashMap, Object...)}
   */
  public static String mergeArrays(
    String... jsonArrays) {

    ArrayList<JsonArray> arrays = new ArrayList<>();

    for (String jsonArray : jsonArrays) {

      arrays.add(
        getJsonArray(jsonArray));
    }

    return (
      mergeArrays(
        "",
        new HashMap<>(),
        arrays.toArray())
        .toString());
  }

  /**
   * Alternative - no object identity keys are set.
   * Seee {@link Json#mergeArrays(String, HashMap, Object...)}
   */
  public static JsonArray mergeArrays(
    Object... jsonArrays) {

    return (
      mergeArrays(
        "",
        new HashMap<>(),
        jsonArrays));
  }

  /**
   * Merge arrays following "Overlay" strategy (overwrite or add).
   * Duplicate elements are added to array until their amount is equal 
   * in both arrays. Objects are considered identical if their
   * identifier-keys are present and their values are equal. If no such 
   * keys, then objects are considered identical on equal content.
   *
   * @param arrayName       Merged arrays name or empty string. 
   *                        Used to choose from key combinations.
   * @param keyCombinations Array objects identifier-key names.
   * @param jsonArrays      Any amount of JSON-arrays to merge.
   *
   * @return Merged array.
   */
  public static JsonArray mergeArrays(
    @NonNull
      String arrayName,
    @NonNull
      HashMap<String, String[]> keyCombinations,
    Object... jsonArrays) {

    JsonArray resultArray = new JsonArray();

    for (Object jsonArray : jsonArrays) {

      JsonArray array = (JsonArray) jsonArray;

      for (JsonElement item : array) {

        if (
          item.isJsonObject() &&
          keyCombinations.get(arrayName) != null &&
          keyCombinations.get(arrayName).length > 0) {

          // Array element is an object with identifier-keys:

          ArrayList<JsonElement> resultArrayObjectsFound =
            getArrayObjectsByKeyValues(
              resultArray,
              item.getAsJsonObject(),
              keyCombinations.get(arrayName));

          if (resultArrayObjectsFound.size() > 0) {

            // Such field is already present, merge is required:

            JsonObject resultArrayObjectFound =
              resultArrayObjectsFound.get(0).getAsJsonObject();

            JsonObject mergedObject =
              mergeObjects(
                keyCombinations,
                resultArrayObjectFound,
                item.getAsJsonObject());

            resultArray.remove(resultArrayObjectFound);
            resultArray.add(mergedObject);

            continue;
          }
        }

        if (!resultArray.contains(item)) {

          // No such element - add:

          resultArray.add(item);
        } else if (
          count(resultArray, item) < count(array, item)) {

          // There are more duplicates of the element - add:

          resultArray.add(item);
        }
      }
    }

    return resultArray;
  }

  /**
   * Convert String to JSON-Array (GSON).
   *
   * @param jsonString JSON-array as string.
   *
   * @return JSON-array as GSON-array.
   */
  public static JsonArray getJsonArray(String jsonString) {

    JsonArray jsonArray = new JsonArray();
    JsonParser parser;

    parser =  new JsonParser();

    try {

      jsonArray =
        parser
          .parse(
            jsonString)
          .getAsJsonArray();

    } catch (Exception ignore) {
    }

    return jsonArray;
  }

  /**
   * Find array objects that have required identity keys and match the values.
   *
   * @param array  Array to search in.
   * @param object Example object for search. 
   *               Contains required keys and values. 
   * @param keys   Object identity keys.
   *
   * @return Matching JSON-elements.
   */
  public static ArrayList<JsonElement> getArrayObjectsByKeyValues(
    JsonArray array,
    JsonObject object,
    String[] keys) {

    ArrayList<JsonElement> elements = new ArrayList<>();

    for (JsonElement arrayElement : array) {

      if (arrayElement.isJsonObject()) {

        JsonObject jsonObject = arrayElement.getAsJsonObject();

        boolean hasAllKeysThatMatch = true;

        for (String key : keys) {

          if (!jsonObject.has(key)) {

            // One of the keys is not found:

            hasAllKeysThatMatch = false;

            break;
          } else {

            if (
              jsonObject.get(key).isJsonPrimitive() &&
              !jsonObject.get(key).equals(object.get(key))) {

              // Primitive type key values don't match:

              hasAllKeysThatMatch = false;

              break;
            }

            if ((
                  jsonObject.get(key).isJsonObject() ||
                  jsonObject.get(key).isJsonArray()) &&
                !jsonObject.get(key).toString().equals(
                  object.get(key).toString())) {

              // Complex type key values don't match:

              hasAllKeysThatMatch = false;

              break;
            }
          }
        }

        if (hasAllKeysThatMatch) {

          // Key values match:

          elements.add(jsonObject);
        }
      }
    }

    return elements;
  }

  /**
   * Count given elements in array.
   *
   * @param element Element to find.
   *
   * @return Amount of given elements in array.
   */
  public static int count(
    JsonArray array,
    JsonElement element) {

    int count = 0;

    for (JsonElement currentElement : array) {

      if (currentElement.isJsonPrimitive()) {

        // Primitive type:

        if (currentElement.equals(element)) {

          count++;
        }
      }

      if (
        currentElement.isJsonObject() ||
        currentElement.isJsonArray()) {

        // Complex type:

        if (currentElement.toString().equals(element.toString())) {

          count++;
        }
      }
    }

    return count;
  }

}
Zon
  • 18,610
  • 7
  • 91
  • 99
0

You can cast one jsonobject to a jsonaray, (you can check here) then you can add new jsonarray to the other jsonobject or convert both jsonobject to a jsonarray, then create a new jsonobject.

jsonObject.add("property", jsonarray);
Luferquisa
  • 73
  • 7