Zwei Möglichkeiten, Gson für JSON in Java zu verwenden

May 07, 2020
Autor:in:

Zwei Möglichkeiten, Gson für JSON in Java zu verwenden


Hallo und Danke fürs Lesen! Dieser Blogpost ist eine Übersetzung von Two ways to use Gson for JSON in Java. Während wir unsere Übersetzungsprozesse verbessern, würden wir uns über Dein Feedback an help@twilio.com freuen, solltest Du etwas bemerken, was falsch übersetzt wurde. Wir bedanken uns für hilfreiche Beiträge mit Twilio Swag :)

Wenn du in einer statisch typisierten Sprache wie Java arbeitest, kann der Umgang mit JSON schwierig sein. JSON hat keine Typdefinitionen und es fehlen einige Funktionen, die wir möchten – es gibt nur Strings, Zahlen, Boolesche Werte und null. Um andere Typen (wie Datum oder Uhrzeit) zu speichern, müssen wir eine auf Strings basierende Konvention verwenden. Trotz seiner Mängel ist JSON das häufigste Format für APIs im Web, also brauchen wir eine Möglichkeit, damit in Java zu arbeiten.

Gson ist eine der beliebtesten Java JSON-Bibliotheken. In diesem Beitrag werde ich ein ziemlich komplexes JSON-Dokument und drei Abfragen auswählen, die ich mit Gson durchführen möchte. Ich werde zwei verschiedene Ansätze vergleichen:

  1. Baummodell
  2. Datenbindung

Der gesamte in diesem Beitrag verwendete Code befindet sich in diesem Repository. Es funktioniert ab Java 8.

Andere Java-Bibliotheken für die Arbeit mit JSON

Die beliebtesten Java-Bibliotheken für die Arbeit mit JSON, gemessen an der Verwendung in Maven Central und GitHub-Stars sind Jackson und Gson. In diesem Beitrag werde ich Gson verwenden. Ich habe auch einen entsprechenden Beitrag mit Jackson-Codebeispielen geschrieben.

Du kannst die Gson-Abhängigkeit für die Beispiele hier sehen.

Beispieldaten und Fragen

Um einige Beispieldaten zu finden, habe ich Tildes letzten Beitrag 7 coole APIs, von denen du nicht wusstest, dass du sie brauchst gelesen und die Near Earth Object Web Service-API aus den NASA-APIs herausgesucht. Diese API wird von dem großartig benannten SpaceRocks-Team gepflegt.

Die NeoWS Feed API-Anforderung gibt eine Liste von allen Asteroiden aus, deren Annäherung an die Erde innerhalb der nächsten 7 Tage erfolgt. Ich werde zeigen, wie die folgenden Fragen in Java beantwortet werden:

  • Wie viele sind es?
    Dies kann durch einen Blick auf den Schlüssel element_count im Stammverzeichnis des JSON-Objekts herausgefunden werden.
  • Wie viele von ihnen sind potenziell gefährlich?
    Wir müssen jedes NEO durchlaufen und den Schlüssel is_potentially_hazardous_asteroid überprüfen, der ein boolescher Wert im JSON ist. (Spoiler: es sind nicht null 😨)
  • Wie heißt das schnellste erdnahe Objekt und wie schnell ist es?
    Wieder müssen wir eine Schleife durchlaufen, aber diesmal sind die Objekte komplexer. Wir müssen uns auch darüber im Klaren sein, dass Geschwindigkeiten als Zeichenfolgen und nicht als Zahlen gespeichert werden, z. B. "kilometers_per_second": "6.076659807". Dies ist in JSON-Dokumenten üblich, da Präzisionsprobleme bei sehr kleinen oder sehr großen Zahlen vermieden werden.

Ein Baummodell für JSON

Mit Gson kannst du JSON in ein Baummodell einlesen: Java-Objekte, die JSON-Objekte, Arrays und Werte darstellen. Diese Objekte heißen z. B. JsonElement oder JsonObject und werden von Gson zur Verfügung gestellt.

Vorteile:

  • Du musst keine eigenen zusätzlichen Klassen erstellen
  • Gson kann einige implizite und explizite Typ-Zwänge für dich ausüben

Nachteile:

  • Dein Code, der mit Gson Baummodellobjekten funktioniert, kann sehr lang sein
  • Es ist sehr verlockend, Gson-Code mit Anwendungslogik zu mischen, was das Lesen und Testen deines Codes erschweren kann

Beispiele für Gson-Baummodelle

Beginne mit der Instanziierung von JsonParser. Dann rufe .parse auf, um ein JsonElement zu bekommen, das durchlaufen werden kann, um verschachtelte Werte abzurufen.  JsonParser ist threadsicher, daher ist es in Ordnung, dasselbe an mehreren Stellen zu verwenden. Der Code zum Erstellen eines JsonElement ist:

JsonParser parser = new JsonParser();
JsonElement neoJsonElement = parser.parse(SourceData.asString());

[Dieser Code im Beispiel-Repo]]

Wie viele NEOs gibt es?

Ich finde diesen Code gut lesbar, obwohl ich eigentlich nicht denke, dass ich über die Unterscheidung zwischen einem JsonElement und einem JsonObject Bescheid wissen muss. Es ist nicht so schlimm hier.

private static int getNeoCount(JsonElement neoJsonElement) {
   return neoJsonElement
       .getAsJsonObject()
       .get("element_count")
       .getAsInt();
}

[Dieser Code im Beispiel-Repo]]

Wenn der Knoten kein Objekt ist, gibt Gson eine IllegalStateException aus, und wenn wir es mit .get() versuchen, um einen Knoten zu erhalten, der nicht existiert, gibt es null aus und wir müssen mögliche NullPointerExceptions selbst behandeln.

Wie viele potenziell gefährliche Asteroiden gibt es diese Woche?

Ich gebe zu, dass ich erwartet hatte, dass die Antwort hier null ist, aber ich habe mich geirrt 19 – Trotzdem bin ich (noch) nicht in Panik. Um dies aus der Root JsonElement zu berechnen, müssen wir Folgendes tun:

  • alle NEOs durchlaufen – es gibt eine Liste von diesen für jedes Datum, so dass wir eine verschachtelte Schleife benötigen
  • einen Zähler erhöhen, wenn das is_potential_hazardous_asteroid Feld true ist

Der Code sieht so aus:

private static int getPotentiallyHazardousAsteroidCount(JsonElement neoJsonElement) {
   int potentiallyHazardousAsteroidCount = 0;
   JsonElement nearEarthObjects = neoJsonElement.getAsJsonObject().get("near_earth_objects");
   for (Map.Entry<String, JsonElement> neoClosestApproachDate : nearEarthObjects.getAsJsonObject().entrySet()) {
       for (JsonElement neo : neoClosestApproachDate.getValue().getAsJsonArray()) {
           if (neo.getAsJsonObject().get("is_potentially_hazardous_asteroid").getAsBoolean()) {
               potentiallyHazardousAsteroidCount += 1;
           }
       }
   }
   return potentiallyHazardousAsteroidCount;
}

[Dieser Code im Beispiel-Repo]]

All diese Aufrufe an .getAsJsonObject() summieren sich zu viel Code (und einer von ihnen ist auch .getAsJsonArray()). JsonObject implementiert nicht Iterable, also wird ein extra Aufruf an .entrySet() für die for-Schleife benötigt. JsonObject hat eine .keys()-Methode, aber nicht .values(), was ich eigentlich in diesem Fall wollte. Insgesamt denke ich, dass die Absicht dieses Codes dadurch verdeckt wird, dass die Gson-API ziemlich langen Code generiert.

Wie heißt das schnellste erdnahe Objekt und wie heißt es?

Die Methode zum Suchen und Durchlaufen jedes erdnahen Objekts ist dieselbe wie im vorherigen Beispiel, aber bei jedem Objekt ist die Geschwindigkeit einige Ebenen tief verschachtelt, so dass du durch diese gehen musst, um den Wert kilometers_per_second auszuwählen.

"close_approach_data": [
 {
    ...
     "relative_velocity": {
         "kilometers_per_second": "6.076659807",
         "kilometers_per_hour": "21875.9753053124",
         "miles_per_hour": "13592.8803223482"
   },
  ...
 }
]

Ich habe eine kleine Klasse erstellt, die beide aufgerufenen Werte NeoNameAndSpeed enthält. Dies könnte ein record in der Zukunft sein. Der Code erstellt eines dieser Objekte:

   private static NeoNameAndSpeed getFastestNEO(JsonElement neoJsonElement) {
   NeoNameAndSpeed fastestNEO = null;
   JsonElement nearEarthObjects = neoJsonElement.getAsJsonObject().get("near_earth_objects");
   for (Map.Entry<String, JsonElement> neoClosestApproachDate : nearEarthObjects.getAsJsonObject().entrySet()) {
       for (JsonElement neo : neoClosestApproachDate.getValue().getAsJsonArray()) {
           double speed = neo.getAsJsonObject()
               .get("close_approach_data").getAsJsonArray()
               .get(0).getAsJsonObject()
               .get("relative_velocity").getAsJsonObject()
               .get("kilometers_per_second")
               .getAsDouble();

           if ( fastestNEO == null ||  speed > fastestNEO.speed ){
               fastestNEO = new NeoNameAndSpeed(neo.getAsJsonObject().get("name").getAsString(), speed);
           }
       }
   }
   return fastestNEO;
}

[Dieser Code im Beispiel-Repo]]

Gson behandelt Zahlen, die als Zeichenfolgen gespeichert sind, durch Aufrufen von Double.parseDouble. Das ist gut so. Aber das ist immer noch viel mehr Code als in der entsprechenden Version mit Jackson. Der Code war umständlich zu schreiben, aber für mich ist der Hauptfehler hier die Lesbarkeit. Aufgrund der Hintergrundgeräusche ist es viel schwieriger zu sagen, was dieser Code bewirkt.

Datenbindung von JSON an benutzerdefinierte Klassen

Wenn du komplexere Abfragen deiner Daten hast oder Objekte aus JSON erstellen musst, die du an anderen Code übergeben kannst, passt das Baummodell nicht gut. Gson bietet eine andere Betriebsart an, Datenbindung, wobei JSON direkt in Objekte deines Designs geparst wird.

Vorteile:

  • Die Konvertierung von JSON in Objekte ist unkompliziert
  • Das Lesen von Werten aus den Objekten kann eine beliebige Java-API verwenden
  • Die Objekte sind unabhängig von Gson und können daher in anderen Kontexten verwendet werden
  • Das Mapping kann mit Typ-Adaptern angepasst werden

Nachteile:

  • Vorarbeiten: Du musst Klassen erstellen, deren Struktur mit den JSON-Objekten übereinstimmt, und dann Gson deine JSON in diese Objekte einlesen lassen.

Eine Einführung in die Datenbindung

Hier ist ein einfaches Beispiel, das auf einer kleinen Teilmenge des NEO JSON basiert:

{
 "id": "54016476",
 "name": "(2020 GR1)",
 "closeApproachDate": "2020-04-12",
}

Wir könnten uns eine Klasse vorstellen, in der diese Daten wie folgt gespeichert sind:

public class NeoSummaryDetails {
    public int id;
    public String name;
    public LocalDate closeApproachDate;
}

Gson ist fast in der Lage, zwischen JSON und passenden Objekten wie diesem sofort hin und her zu ordnen. Es kommt gut mit int id zurecht, das ein String ist, braucht aber Hilfe beim Konvertieren des Strings 2020-04-12 zu einem LocalDate-Objekt. Dazu wird eine Klasse erstellt, die TypeAdaptor<LocalDate> erweitert und die .read-Methode zum Aufrufen von LocalDate.parse übergeht. Du kannst hier ein Beispiel dafür sehen.

Gson-Datenbindung – benutzerdefinierte Typen

Für die Datenbindung ist die zu verwendende Gson-Klasse Gson. Sobald wir unseren TypeAdapter  erstellt haben, können wir ihn in einem Gson wie folgt registrieren:

   Gson gson = new GsonBuilder()
       .registerTypeAdapter(LocalDate.class, new GsonLocalDateAdapter())
       .create();

[Dieser Code im Beispiel-Repo]]

Gson-Datenbindung – benutzerdefinierte Feldnamen

Du hast vielleicht bemerkt, dass ich closeApproachDate in meinem kurzen Beispiel-JSON oben verwendet haben, wo die Daten von der NASA close_approach_date haben. Ich habe das gemacht, weil Gson die Reflexionsfähigkeiten von Java verwendet, um JSON-Schlüssel mit Java-Feldnamen abzugleichen, und sie müssen genau übereinstimmen.

In den meisten Fällen kannst du deinen JSON nicht ändern – normalerweise stammt er von einer API, die du nicht steuerst – aber du möchtest trotzdem nicht, dass Felder in deinen Java-Klassen in snake_case geschrieben werden. Dies hätte mit einer Anmerkung auf dem closeApproachDate Feld gemacht werden können:

@SerializedName("close_approach_date")
public LocalDate closeApproachDate;

[Dieser Code im Beispiel-Repo]]

Erstellen von benutzerdefinierten Objekten mit JsonSchema2Pojo

Im Moment denkst du wahrscheinlich, dass dies sehr zeitaufwändig werden kann. Feldumbenennung, benutzerdefinierte Leser und Schreiber, ganz zu schweigen von der schieren Anzahl von Klassen, die du möglicherweise erstellen musst.  Nun, du hast recht! Aber keine Angst, es gibt ein großartiges Tool, um die Klassen für dich zu erstellen.

JsonSchema2Pojo kann ein JSON-Schema oder (sinnvoller) ein JSON-Dokument nehmen und passende Klassen für dich generieren. Es kennt Gson-Anmerkungen und verfügt über unzählige Optionen, obwohl die Standardeinstellungen sinnvoll sind. Normalerweise finde ich, dass es 90 % der Arbeit für mich erledigt, aber die Klassen brauchen oft etwas Feinschliff, sobald sie generiert sind.

Um es für dieses Projekt zu verwenden, habe ich alle bis auf eines der NEOs entfernt und die folgenden Optionen ausgewählt:

JsonSchema2Pojo Screenshot

[Generierter Code im Beispiel-Repo]]

In Schlüsseln und Werten gespeicherte Daten

Der NeoWS JSON verfügt über eine etwas umständliche (aber nicht ungewöhnliche) Funktion – einige Daten werden in Schlüsseln anstatt von Werten der JSON-Objekte gespeichert. Die near_earth_objects-Karte hat Schlüssel, die dates sind. Dies ist ein kleines Problem, da die Daten nicht immer gleich sind, aber jsonschema2pojo weiß das natürlich nicht. Es hat ein Feld namens _20200412 erstellt. Um dies zu beheben, habe ich die Klasse _20200412 zu NeoDetails umbenannt und der Typ von nearEarthObjects wurde zu Map<String, List<NeoDetails>> (siehe hier). Ich konnte dann die jetzt unbenutzte NearEarthObjects-Klasse löschen.

Ich habe auch die Arten von Zahlen in Strings von String zu double geändert und wo nötig LocalDate hinzugefügt.

Gson-Datenbindung für die Near-Earth Object API

Mit den von JsonSchema2Pojo generierten Klassen kann das gesamte große JSON-Dokument gelesen werden mit:

NeoWsDataGson neoWsDataGson = new GsonBuilder()
   .registerTypeAdapter(LocalDate.class, new GsonLocalDateAdapter())
   .create()
   .fromJson(SourceData.asString(), NeoWsDataGson.class);

 

Finden der gewünschten Daten

Jetzt, da wir einfache Java-Objekte haben, können wir den normalen Feldzugriff und die Streams-API verwenden, um die gewünschten Daten zu finden. Dieser Code ist der gleiche für Gson oder Jackson:

System.out.println("NEO count: " + neoWsData.elementCount);


System.out.println("Potentially hazardous asteroids: " +
   neoWsData.nearEarthObjects.values()
       .stream().flatMap(Collection::stream) // this converts a Collection of Collections of objects into a single stream
       .filter(neo -> neo.isPotentiallyHazardousAsteroid)
       .count());


NeoDetails fastestNeo = neoWsData.nearEarthObjects.values()
   .stream().flatMap(Collection::stream)
   .max( Comparator.comparing( neo -> neo.closeApproachData.get(0).relativeVelocity.kilometersPerSecond ))
   .get();

System.out.println(String.format("Fastest NEO is: %s at %f km/sec",
   fastestNeo.name,
   fastestNeo.closeApproachData.get(0).relativeVelocity.kilometersPerSecond));

[Dieser Code im Beispiel-Repo]]

Dieser Code ist natürlicheres Java und enthält nicht alle Funktionen von Gson, so dass es einfacher wäre, diese Logik einem Unit-Test zu unterziehen. Wenn du häufig mit demselben JSON-Format arbeitest, lohnt sich die Investition in die Erstellung von Klassen wahrscheinlich.

Pfadabfragen mit Gson

Wenn du meinen Beitrag über Arbeiten mit JSON unter Verwendung von Jackson gelesen hast, hast du möglicherweise den Abschnitt zum Abrufen einzelner Werte aus einem JSON-Dokument mithilfe von JSON-Zeigern gelesen. Gson unterstützt JSON-Zeiger nicht. Ich halte dies jedoch nicht für einen großen Verlust, da normalerweise nicht viel Code gespeichert wird und es flexiblere Alternativen gibt.

Zusammenfassung der verschiedenen Verwendungsmöglichkeiten von Gson

Für einfache Abfragen kann dir das Baummodell gute Dienste leisten, aber du wirst höchstwahrscheinlich die JSON-Parsing- und Anwendungslogik durcheinanderbringen, was das Pflegen des Codes erschweren kann.

Für komplexere Abfragen und insbesondere, wenn dein JSON-Parsing Teil einer größeren Anwendung ist, empfehle ich Datenbindung. Auf lange Sicht ist dies normalerweise am einfachsten, wenn man bedenkt, dass JsonSchema2Pojo den größten Teil der Arbeit für dich erledigen kann.

Wie arbeitest du am liebsten mit JSON in Java? Lass es mich auf Twitter wissen @ MaximumGilliard oder per E-Mail: ich mgilliard@twilio.com. Ich bin gespannt, von {"deinen": "Entwicklungen"}. zu hören.