Using Jackson to Remove Empty JSON Fields

  • Post Author:
  • Post Category:Java

I recently had a use case for cleaning out a JSON object and removing empty values. This was related to merging JSON where an empty key-value pair or place in an array meant the field should be removed. With the help of Jackson I was able to create two recursive methods that work together to git ‘er done.

Approach

For me the key was breaking the problem up into two possibilities: the root node is either an ObjectNode or an ArrayNode. I handled both of these cases separately, writing a method that works for object-only JSON first and then one that works for array-only JSON. I wanted to be able walk through the node without messing things up so I decided to rebuild the JSON as I went. As you will see below I use an ObjectMapper to create new JsonNodes. Unfortunately this is an expensive process so in practice the mapper would probably be best as a global variable (ew!).

Seeking the purest form of recursion possible I did try and get everything into one method but the object type for the new JSON got a little hairy.  Moving back and forth between a general JsonNode to more specific ObjectNodes and ArrayNodes made for some ugly code with a lot of casting.

Once both methods were done I updated them to call each other, depending on what the child elements were. Both methods dive into each element of the JSON, inspecting values (the simplest case) or (sometimes recursively) calling either the function that handles ObjectNodes or the function that handles ArrayNodes.

Code

    /**
     * Removes empty fields from the given JSON object node.
     * @param an object node
     * @return the object node with empty fields removed
     */
    public static ObjectNode removeEmptyFields(final ObjectNode jsonNode) {
        ObjectNode ret = new ObjectMapper().createObjectNode();
        Iterator<Entry<String, JsonNode>> iter = jsonNode.fields();

        while (iter.hasNext()) {
            Entry<String, JsonNode> entry = iter.next();
            String key = entry.getKey();
            JsonNode value = entry.getValue();

            if (value instanceof ObjectNode) {
                Map<String, ObjectNode> map = new HashMap<String, ObjectNode>();
                map.put(key, removeEmptyFields((ObjectNode)value));
                ret.setAll(map);
            }
            else if (value instanceof ArrayNode) {
                ret.set(key, removeEmptyFields((ArrayNode)value));
            }
            else if (value.asText() != null && !value.asText().isEmpty()) {
                ret.set(key, value);
            }
        }

        return ret;
    }


    /**
     * Removes empty fields from the given JSON array node.
     * @param an array node
     * @return the array node with empty fields removed
     */
    public static ArrayNode removeEmptyFields(ArrayNode array) {
        ArrayNode ret = new ObjectMapper().createArrayNode();
        Iterator<JsonNode> iter = array.elements();

        while (iter.hasNext()) {
            JsonNode value = iter.next();

            if (value instanceof ArrayNode) {
                ret.add(removeEmptyFields((ArrayNode)(value)));
            }
            else if (value instanceof ObjectNode) {
                ret.add(removeEmptyFields((ObjectNode)(value)));
            }
            else if (value != null && !value.textValue().isEmpty()){
                ret.add(value);
            }
        }

        return ret;
    }

 

Tests

Apparently I like eating so much that whenever I need test data it always ends up being about food. Today it’s what my shopping cart looks like.

Before

 
   "number-of-wheels":4,
   "metal-type":"",
   "food": 
      "apple",
      "",
      "orange",
       
         "jelly-beans": 
            "coconut",
            "lemon",
            ""
         ]
      }
   ]
}

After

 
   "number-of-wheels":4,
   "food": 
      "apple",
      "orange",
       
         "jelly-beans": 
            "coconut",
            "lemon"
         ]
      }
   ]
}

Code

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.junit.Assert.*;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

   @Test
    public void testRemoveEmptyFields() throws JsonProcessingException {
        final ObjectMapper mapper = new ObjectMapper();

        final ArrayNode jellyBeans = mapper.createArrayNode();
        jellyBeans.add("coconut");
        jellyBeans.add("lemon");
        jellyBeans.add("");
 
        final ObjectNode jellyBeanBag = mapper.createObjectNode();
        jellyBeanBag.set("jelly-beans", jellyBeans);

        final ArrayNode food = mapper.createArrayNode();
        food.add("apple");
        food.add("");
        food.add("orange");
        food.add(jellyBeanBag);

        final ObjectNode shoppingCart = mapper.createObjectNode();
        shoppingCart.put("number-of-wheels", 4);
        shoppingCart.put("metal-type", "");
        shoppingCart.putArray("food").addAll(food);

        LOGGER.info(mapper.writeValueAsString(shoppingCart));

        final JsonNode result = JsonUtils.removeEmptyFields(shoppingCart);
        LOGGER.info(mapper.writeValueAsString(result));

        assertTrue(result.has("number-of-wheels"));
        assertFalse(result.has("metal-type"));
        assertTrue(result.has("food"));

        final JsonNode foodResult = result.get("food");
        assertTrue(foodResult instanceof ArrayNode);

        final ArrayNode foodResultArray = (ArrayNode)foodResult;
        assertTrue(foodResultArray.size() == 3);
        assertTrue(foodResultArray.get(0).textValue().equals("apple"));
        assertTrue(foodResultArray.get(1).textValue().equals("orange"));
        assertTrue(foodResultArray.has(2));
        assertTrue(foodResultArray.get(2) instanceof ObjectNode);

        final ObjectNode jellyBeanBagResult = (ObjectNode)foodResultArray.get(2);
        assertTrue(jellyBeanBagResult.has("jelly-beans"));
 
        final JsonNode jellyBeansResult = jellyBeanBagResult.get("jelly-beans");
        assertTrue(jellyBeansResult instanceof ArrayNode);

        final ArrayNode jellyBeansResultArray = (ArrayNode)jellyBeansResult;
        assertTrue(jellyBeansResultArray.size() == 2);
        assertTrue(jellyBeansResultArray.get(0).textValue().equals("coconut"));
        assertTrue(jellyBeansResultArray.get(1).textValue().equals("lemon"));

    }