Parsing nested data structures can be tricky. This README shares multiple ways to parse complex objects along with the common pitfalls, pros and cons.
class LevelOne {
String levelOneName;
LevelTwo levelTwo;
}
class LevelTwo {
String levelTwoName;
List<LevelThree> levelThreeList;
}
class LevelThree {
String levelThreeName;
Map<String, LevelFour> levelFourMap:
}
class LevelFour {
String levelFourName;
}
While levelFourName can be retrieved using a chain of getters, this is an extremely bad way because NullPointerException is thrown when one of the objects is null.
public String getName(LevelOne levelOne) {
return levelOne.getLevelTwo().getLevelThreeList().get(0).getLevelFourMap().get("key").getLevelFourName();
return levelOne
.getLevelTwo() // NullPointerException is thrown if levelOne is null, or so on...
.getLevelThreeList()
.get(0)
.getLevelFourMap()
.get("key")
.getLevelFourName();
}
Unless it is certain that none of the objects will ever be null (which is rarely the case!), it wouldn’t hurt to include exception handling or null checks.
With exception handling:
public String getName(LevelOne levelOne) {
try {
return levelOne.getLevelTwo().getLevelThreeList().get(0).getLevelFourMap().get("key").getLevelFourName();
return levelOne
.getLevelTwo()
.getLevelThreeList()
.get(0)
.getLevelFourMap()
.get("key")
.getLevelFourName();
} catch (Exception e) {
return null;
}
In this method, we concatenate a list of null checks using && condition. If the list of conditions is true, then it would be safe to get levelFourName using a chain of getters.
public String getName(LevelOne levelOne) {
if (
levelOne != null
&& levelOne.getLevelTwo() != null
&& levelOne.getLevelTwo().getLevelThreeList() != null
&& !levelOne.getLevelTwo().getLevelThreeList().empty()
&& levelOne.getLevelTwo().getLevelThreeList().get(0).getLevelFourMap() != null
&& levelOne.getLevelTwo().getLevelThreeList().get(0).getLevelFourMap().contains("key")
&& levelOne.getLevelTwo().getLevelThreeList().get(0).getLevelFourMap().get("key") != null
) {
return levelOne
.getLevelTwo()
.getLevelThreeList()
.get(0)
.getLevelFourMap()
.get("key")
.getLevelFourName();
}
}
So far, we’ve only been able to retrieve a single field levelFourName. To retrieve multiple fields (e.g. levelTwoName and levelFourName), we will need to use multiple chain of getters.
public List<String> getNames(LevelOne levelOne) {
List<String> result = new ArrayList<>();
if (
levelOne != null
&& levelOne.getLevelTwo() != null
&& levelOne.getLevelTwo().getLevelThreeList() != null
&& !levelOne.getLevelTwo().getLevelThreeList().empty()
&& levelOne.getLevelTwo().getLevelThreeList().get(0).getLevelFourMap() != null
&& levelOne.getLevelTwo().getLevelThreeList().get(0).getLevelFourMap().contains("key")
&& levelOne.getLevelTwo().getLevelThreeList().get(0).getLevelFourMap().get("key") != null
) {
// Add LevelTwoName to result
result.add(levelOne
.getLevelTwo()
.getLevelTwoName();
// Add LevelFourName to result
result.add(levelOne
.getLevelTwo()
.getLevelThreeList()
.get(0)
.getLevelFourMap()
.get("key")
.getLevelFourName());
}
return result;
}
This method is inefficient considering that we are calling getters on same objects multiple times. Plus, we are unable to retrieve levelTwoName when levelFourMap or levelFour is null. To handle this scenario, we will need to create two almost similar looking methods getLevelTwoName() and getLevelFourName(), which again will be calling repeated getters which is really inefficient.
The Optional class was introduced in Java 8. While this method follows proper coding convention, it can be difficult to write and debug. I.e. when to use map, filter, stream, get, orElse, etc…? Plus, the behaviour and return type of each method is not immediately obvious.
public getNames(LevelOne levelOne) {
return Optional.ofNullable(levelOne)
.map(LevelOne::getLevelTwo)
.map(LevelTwo::getLevelThreeList)
.filter(list -> !list.empty())
.map(list -> list.get(0))
.map(LevelThree::getLevelFourMap)
.map(map -> map.get("key"))
.map(LevelFour::getName)
.orElse(null);
}
First, we will need to implement the StatementHandler and StaticStatementHandler classes. We can put them in a util package as a common utility class:
package com.example.common.util;
/**
* This class contains several closures for wrapping chained method calls. The closure can
* execute either an optional call chain. If an optional call chain fails with an NPE, a null will be returned.
*/
@Component
public class StatementHandler {
public <T> T optional(Supplier<T> statement) {
return StaticStatementHandler.optional(statement);
}
}
public final class StaticStatementHandler {
public static <T> T optional(Supplier<T> statement) {
try {
return statement.get();
} catch (Exception exc) {
return null;
}
}
}
Then, we can make use of StatementHandler.optional() to parse our nested object:
package com.example.levelone;
import com.example.common.util.StatementHandler;
public class LevelOneController {
@Autowired
StatementHandler statementHandler;
public String getName(LevelOne levelOne) {
return statementHandler.optional(() ->
levelOne
.getLevelTwo()
.getLevelThreeList()
.get(0)
.getLevelFourMap()
.get("key")
.getLevelFourName();
);
}
}
This method basically abstracts the try and catch exception block into the StaticStatementHandler class so that it is safe to use the chain of getters method.
This method traverses the nested data structure level by level. This allows us to retrieve levelTwoName even if the remaining nested objects are null (e.g. if levelFourMap is null). However, this method has poor readability and certainly does not sit well with Never Nesters.
public List<String> getNames(LevelOne levelOne) {
List<String> result = new ArrayList<>();
if (levelOne != null) {
LevelTwo levelTwo = levelOne.getLevelTwo();
if (levelTwo != null) {
// Add levelTwoName to result
result.add(levelTwo.getLevelTwoName());
List<LevelThree> levelThreeList = levelTwo.getLevelThreeList();
if (levelThreeList != null && !levelThreeList.empty()) {
Map<String, LevelFour> levelFourMap = levelThreeList.get(0).getLevelFourMap();
if (levelFourMap != null && levelFourMap.contains("key")) {
LevelFour levelFour = levelFourMap.get("key");
if (levelFour != null) {
// Add levelFourName to result
result.add(levelFour.getLevelFourName());
}
}
}
}
}
return result;
}
We can flatten the traversal using the following two options.
Option 1: Skip assignment if null
public List<String> getNames(LevelOne levelOne) {
List<String> result = new ArrayList<>();
LevelTwo levelTwo;
if (levelOne != null) {
levelTwo = levelOne.getLevelTwo();
}
List<LevelThree> levelThreeList;
if (levelTwo != null) {
// Add levelTwoName to result
result.add(levelTwo.getLevelTwoName());
levelThreeList = levelTwo.getLevelThreeList();
}
Map<String, LevelFour> levelFourMap;
if (levelThreeList != null && !levelThreeList.empty()) {
levelFourMap = levelThreeList.get(0).getLevelFourMap();
}
LevelFour levelFour;
if (levelFourMap != null && levelFourMap.contains("key")) {
levelFour = levelFourMap.get("key");
}
if (levelFour != null) {
// Add levelFourName to result
result.add(levelFour.getLevelFourName());
}
return result;
}
Option 2: Return null immediately if null
public List<String> getNames(LevelOne levelOne) {
List<String> result = new ArrayList<>();
if (levelOne == null) {
return null;
}
LevelTwo levelTwo = levelOne.getLevelTwo();
if (levelTwo == null) {
return null;
}
// Add levelTwoName to result
result.add(levelTwo.getLevelTwoName());
List<LevelThree> levelThreeList = levelTwo.getLevelThreeList();
if (levelThreeList == null || levelThreeList.empty()) {
return null;
}
Map<String, LevelFour> levelFourMap = levelThreeList.get(0).getLevelFourMap();
if (levelFourMap == null || !levelFourMap.contains("key")) {
return null;
}
LevelFour levelFour = levelFourMap.get("key");
if (levelFour == null) {
return null;
}
// Add levelFourName to result
result.add(levelFour.getLevelFourName());
return result;
}
Option 3: Return result immediately if null
public List<String> getNames(LevelOne levelOne) {
List<String> result = new ArrayList<>();
if (levelOne == null) {
return result;
}
LevelTwo levelTwo = levelOne.getLevelTwo();
if (levelTwo == null) {
return result;
}
// Add levelTwoName to result
result.add(levelTwo.getLevelTwoName());
List<LevelThree> levelThreeList = levelTwo.getLevelThreeList();
if (levelThreeList == null || levelThreeList.empty()) {
return result;
}
Map<String, LevelFour> levelFourMap = levelThreeList.get(0).getLevelFourMap();
if (levelFourMap == null || !levelFourMap.contains("key")) {
return result;
}
LevelFour levelFour = levelFourMap.get("key");
if (levelFour == null) {
return result;
}
// Add levelFourName to result
result.add(levelFour.getLevelFourName());
return result;
}
Option 2 and Option 3 has better readability and is straighforward – immediately return null or result once a null object is found.
However, Option 1 is better for two reasons:
levelTwoName exists, but levelFourMap or levelFour objects are null, which cannot be achieved using option 2.levelTwo does not exist, but levelFourName exists, which cannot be achieved using option 3.Unit tests for option 1 (1 unit tests, 1 helper method, 100% code coverage):
@Test
public void getNamesTest_shouldReturnListWithTwoStrings() {
LevelOne levelOne = LevelUtil.getLevelOne();
List<String> result = getNames(levelOne));
assertEquals(2, result.size());
assertEquals("levelTwoName", result.get(0));
assertEquals("levelFourName", result.get(1));
}
Unit tests for option 2 (5 unit tests, 4 helper methods, 100% code coverage):
@Test
public void getNamesTest_levelOneNull_shouldReturnNull {
LevelOne levelOne = null;
assertNull(getName(levelOne));
}
@Test
public void getNamesTest_levelTwoNull_shouldReturnNull {
LevelOne levelOne = LevelUtil.getLevelOneWithLevelTwoNull();
assertNull(getName(levelOne));
}
@Test
public void getNamesTest_levelThreeNull_shouldReturnListWithOneString {
LevelOne levelOne = LevelUtil.getLevelOneWithLevelThreeListNull();
List<String> result = getNames(levelOne));
assertEquals(1, result.size());
assertEquals("levelTwoName", result.get(0));
}
@Test
public void getNamesTest_levelFourNull_shouldReturnListWithOneString {
LevelOne levelOne = LevelUtil.getLevelOneWithLevelFourMapNull();
List<String> result = getNames(levelOne));
assertEquals(1, result.size());
assertEquals("levelTwoName", result.get(0));
}
@Test
public void getNamesTest_levelFourNull_shouldReturnListWithTwoStrings {
LevelOne levelOne = LevelUtil.getLevelOne();
List<String> result = getNames(levelOne));
assertEquals(2, result.size());
assertEquals("levelTwoName", result.get(0));
assertEquals("levelFourName", result.get(1));
}
LevelUtil:
public class LevelUtil {
public static LevelOne getLevelOne() {
// LevelOne
LevelOne levelOne = new LevelOne();
// LevelTwo
LevelTwo levelTwo = new LevelTwo();
levelTwo.setName("levelTwoName");
levelOne.setLevelTwo(levelTwo);
// LevelThree
List<LevelThree> levelThreeList= new ArrayList<>();
LevelThree levelThree = new LevelThree();
levelThreeList.add(levelThree);
levelTwo.setLevelThreeList(levelThreeList);
// LevelFour
Map<String, LevelFour> levelFourMap = new HashMap<>();
LevelFour levelFour = new LevelFour();
levelFour.setName("levelFourName");
levelThree.setLevelFourMap(levelFourMap);
return levelOne;
}
public static LevelOne getLevelOneWithLevelTwoNull() {
LevelOne levelOne = getLevelOne();
levelOne.setLevelTwo(null);
return levelOne;
}
public static LevelOne getLevelOneWithLevelThreeListNull() {
LevelOne levelOne = getLevelOne();
levelOne.getLevelTwo().setLevelThreeList(null);
return levelOne;
}
public static LevelOne getLevelOneWithLevelFourMapNull() {
LevelOne levelOne = getLevelOne();
levelOne.getLevelTwo().getLevelThreeList().get(0).setLevelFourMap(null);
return levelOne;
}
}