Auf einsamen Pfaden mit Jackson oder wie man mit einem JsonPointer punkten kann

Geh nicht immer auf dem vorgezeichneten Weg, der nur dahin führt, wo andere bereits gegangen sind.

Alexander Graham Bell

Immer seltener passiert es, dass ein Entwickler sich mit der Bibliothek Jackson beschäftigen muss. Viele Frameworks, wie etwa Jersey und Spring Boot verwenden die Bibliothek intern zum Mappen zwischen JSON und ihren POJOs. Dabei greifen sie auf die Möglichkeiten des Jackson ObjektMappers zu, unter Angabe einer Klasse und einer JSON Quelle, fertig gefüllte Instanzen dieser Klasse zu erzeugen.

Person person = new ObjectMapper().readValue("{\"lastname\":"who",\"title\":"doctor"}", Person.class);

Hin und wieder aber, muss man in das wilde Gestrüpp der JSON Daten greifen, um eine notwendige Information zu erhalten. Entweder der Faulheit des Entwicklers geschuldet, eine passende Domänenklasse für das Mapping zu schreiben, oder der Kreativität anderer, ein völlig semantikbefreites Format ersonnen zu haben.

Dann greift der Entwickler direkt auf den Baum von JsonNode Instanzen zu, navigiert darin bis zum Ziel und entnimmt dort die gewünschte Information.

In unserem, wieder einmal, völlig realitätsfernen Beispiel benötigen wir die Anrede aus einem Briefkopf eines Briefes aus einer Vorlagenmappe.

JsonNode source= mapper.readTree(source);
String anrede = "";
if (source.has("vorlagenmappe")) {
  JsonNode vorlagenMappe = source.get("vorlagenmappe");
  if (vorlagenMappe.has("brief")) {
    JsonNode brief = vorlagenMappe.get("brief");
    if (brief.has("briefkopf")) {
      JsonNode briefKopf = brief.get("briefkopf");
      if (briefKopf.has("anrede")) {
        anrede = briefKopf.get("anrede").asText();
      }
    }
  }
}
return anrede;

Was eben noch wie ein ausgeklügelter Plan der IMF klang, sieht mit diesem Sourcecode aus, wie der Belagerungsplan von Troja. Mit jeder weiteren Stufe in der Hierarchie, schraubt sich das if Konstrukt immer weiter in die Tiefe.

Selbstverständlich ist dieses ein rein didaktisches Negativbeispiel und auch bei diesem Ansatz kann es schönere Lösungen geben.

JsonNode source= mapper.readTree(source);
JsonNode vorlagenMappe = source.get("vorlagenmappe");
if (vorlagenMappe == null) {
  return "";
}
JsonNode brief = vorlagenMappe.get("brief");
if (brief == null) {
  return "";
}
JsonNode briefKopf = brief.get("briefkopf");
if (briefKopf == null) {
  return "";
}  
JsonNode anrede = briefKopf.get("anrede"):
return anrede == null ? "": anrede.asText();

Die tiefe Verschachtelung ist verschwunden und der doppelte Zugriff auf die JsonNode ist auch nicht mehr vorhanden. Das Prüfen mit has() ist nämlich auch nur ein weiterer get() Zugriff mit Null-Prüfung. Jahrelange Erfahrung zeigt aber, dass der menschliche Geist lieber Treppen hinabsteigt und daher die erste Variante sehr viel häufiger anzutreffen ist.

Aber schön ist das noch immer nicht. Ein kleines Juwel in der Jackson API ermöglicht eine sehr elegante Lösung. Sie scheint vielen Entwicklern umbekannt zu sein und ich vermute, es liegt einfach an der Kürze ihres Namens.

Die Jackson Klasse JsonNode stellt die Methode at() zur Verfügung, die entweder mit einem JsonPointer oder der String Darstellung eines JsonPointers aufgerufen werden kann. Der JsonPointer definiert einen Pfad durch die JSON Hierachie.

Unser Beispiel unter Verwendung eines JsonPointer.

JsonNode vorlagenMappe = mapper.readTree(source);
return vorlagenMappe.at("/vorlagenmappe/brief/briefkopf/anrede").asText();

Eine weitere Besonderheit der Methode at() ist, dass sie immer einen JsonNode zurückliefert. Selbst dann, wenn es dort keinen JsonNode geben sollte. Hier verwendet Jackson das Null Design Pattern und liefert einen MissingNode zurück, wenn es keinen existierenden Knoten gibt.

Wer sich das nächste Mal einen Weg durch das JSON Gestrüpp bahnt, sollte vielleicht umkehren und einen Pfad mit JsonPointer verwenden.