Introduction

Suppose we have a JSON file like below and we want to convert to into a map (a hash in Ruby terminology.)

{
  "first": {
    "second": {
      "third": 1881
    }
  }
}

Ruby has a dig() method which provides a null-safe way to go deep in a nested map and Groovy has a safe navigation syntax to do the same.

Below 2 examples would return the value 1881

Ruby usage
map.dig('first', 'second', 'third') # returns 1881
map.dig('first', 'X', 'third') # returns ""
Groovy usage
map.get('first')?.get('second')?.get('third') // returns 1881
map.get('first')?.get('X')?.get('third') // returns "null"

Unfortunately, Java doesn’t provide similar syntax. We would write something like below in Java:

Java 7 and below
// it's just too verbose, let's skip it...
Java 8+
var em = Collections.emptyMap();
map.getOrDefault("first", em).getOrDefault("second", em).getOrDefault("third", em)

Ruby’s dig method in Java

We will implement an equivalent of Ruby’s dig method in Java which will be used as follows:

Java usage
dig(map, "first", "second", "third")) // retuns Optional[1881]
dig(map, "first", "X", "third")) // returns Optional.empty
Implementing the dig method in Java
class MapUtils { (1)

    public static <R> Optional<R> dig(Map<String, ?> map, String... keys) { (2)

        if (map == null) {
            throw new IllegalArgumentException("'map' cannot be null!");
        }

        if (keys == null || keys.length == 0) {
            throw new IllegalArgumentException("You should provide at least one key!");
        }

        Map<String, ?> currentMap = map;

        for (int i = 0; i < keys.length; ++i) {
            final String key = keys[i];
            final Object value = currentMap.get(key);
            if (value instanceof Map) { (3)
                currentMap = (Map<String, ?>) value; (4)
            }
            else {
                if (i == keys.length - 1) { (5)
                    final R retVal = (R) value; (6)
                    return Optional.ofNullable(retVal);
                }
                return Optional.empty();
            }
        }

        return Optional.empty();
    }
}
1 You can put this method into any class and it has not to be a static method. This is just for convention.
2 <R> is a generic type. dig method has a parameterized return type so that developers can use it without casting.
3 No need for extra null checking because instanceof would return false for `null`s.
4 We need to assume that the value (the next object in the map) is of type Map<String, ?>. This is a safe assumption for us because if the key of the new map is not a string or the value of it is not a Map we won’t have an error in next loop and simply return Optional.empty().
5 If the value is not a Map and we have just used the last key then it means that we have got the final value.
6 We need to cast the return value. Here, the type safety is gone and successful execution is dependent on the developer’s correct assumption of the result type.

Remember the sample.json the last value is an integer 1881 not a string "1881". Hence:

Optional<String> result = dig(map, "first", "second", "third");
String year = result.get();
// Exception: java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String

// Correct usage should be:
Optional<Integer> result = dig(map, "first", "second", "third");
Integer year = result.get();

Trying the null safe navigation for Java HashMap

  1. Clone the and import the project into your IDE

  2. Run DigMethodShowCase class

Or execute in command line

  1. Build the project with mvn clean package command

    1. Run this command in project’s root (where the pom.xml resides)

  2. Execute it via:

java -jar target/ruby-dig-method-for-java-map-0.0.1-SNAPSHOT-jar-with-dependencies.jar

Seeing the original behavior

There is also a Ruby file to demonstrate the original dig method in src/main/resources/sample.rb

If you have Ruby installed you can run the file via

$ cd src/main/resources
$ ruby sample.rb

It will parse sample.json into a map (Ruby folks call it hash) and use dig on it.

Comments