Stream API allMatch() with mulitple nullable nested objects

Viewed 250

I'm working on a request validation feature where I need to check if a certain string value is present in a property that is contained in an object, which is contained as a value of a map entry, that map is a part of an object and finally my request body contains a set of those objects.

To try and make it clearer I will recreate my situation using only parts of code that are important.

Lets say my ClassA is received in request body, it has following properties:

@Nullable
Set<ClassB> mySet = null;

Now, we have ClassB that has a certain wrapper object of type ClassC:

@Nullable 
ClassC classC;

That wrapper class ClassC contains a map:

@Nullable
Map<String, ClassD> classDMap = new HashMap<>;

Finally, ClassD contains the property that needs to be checked if it contains a certain value:

@Nullable
String propertyForValidation;

At the moment I went with code that looks like this:

public boolean isValid(ClassA body) {
  return body.getMySet().stream()
                        .allMatch(x -> x.getClassC().getClassDMap().values().stream()
                        .allMatch(y -> y.getPropertyForValidation.equals("someValue"));
}

Current problem with my code is that it does not work as expected when some objects that I've described above are null since I'm getting a lot of NullPointerExceptions. I suppose this can be resolved with a lot ifs and null checks, but I was wondering if there is a more elegant way of doing it using Stream API?

All help is appreciated, thanks.

3 Answers

I'd try making use of Stream.filter(). Something like Stream.filter(x -> x!=null) should work instead of (and be more elegant than) if statements.

In your case it would be necessary before every one of your object or field (that is nullable) getters I'm afraid. But that's the most streamish' way I'd go for.

You can also use it in conjuction with the isEmpty() method for cases of empty maps.

An alternative would be to add if statements within your getter functions. That would allow you to keep the exact stream you wrote already.

Here is a solution using a single Stream. It is a bit repetitive but should comply with your expected results.

Note the usage of:

  • Objects#nonNull to check the nullity of an object using a method reference
  • The way the literal String is used first with equals to avoid the last NullPointerException possible

public boolean isValid(ClassA body) {
    return ofNullable(body.getMySet())
               .orElse(emptySet())
               .stream()
               .filter(Objects::nonNull)
               .map(ClassB::getClassC)
               .filter(Objects::nonNull)
               .map(ClassC::getClassDMap)
               .filter(Objects::nonNull)
               .flatMap(map -> map.values().stream())
               .allMatch(y -> "someValue".equals(y.getPropertyForValidation()));
}

Inspired by the comment of @Turing85, I tried to work this out with Optionals. I would not recommend this method but figured it could be educational to share here!

The effect of each orElse(Boolean.TRUE) is to return true when the respective component is missing/set is empty (if a missing component should fail validation, change this to Boolean.FALSE). If we can assume that no null values are ever set on the Map of ClassD, then Stream::allMatch can work directly on ClassD instead of Optional<ClassD>. Luckily, the final String::equals comparison provides one null check for free against the value returned by ClassD::getPropertyForValidation.

public boolean isValid(ClassA body) {
   return Optional.ofNullable(body)
           .flatMap(a -> Optional.ofNullable(a.getMySet()))
           .map(Set::stream)
           .map(streamB -> streamB
                   .map(b -> Optional.ofNullable(b.getClassC()))
                   .map(opC -> opC.flatMap(c -> Optional.ofNullable(c.getClassDMap())))
                   .map(opMapD -> opMapD
                           .map(map -> map.values().stream()
                                   .map(d -> Optional.ofNullable(d))
                                   .allMatch(opD -> opD
                                           .map(d -> "someValue".equals(d.getPropertyForValidation()))
                                           .orElse(Boolean.TRUE)))
                           .orElse(Boolean.TRUE))
                   .reduce((acc, item) -> acc && item)
                   .orElse(Boolean.TRUE))
           .orElse(Boolean.TRUE);
}
Related