Wie Trello Android von Gson zu Mosha wurde

Trello Android wurde kürzlich eingestellt Gson zu Moshi für die JSON-Verarbeitung. Es war ein bisschen schwierig, also wollte ich den Prozess dokumentieren.

(Für den Kontext analysiert Trello Android hauptsächlich JSON. JSON wird selten serialisiert, daher liegt der Schwerpunkt hier hauptsächlich auf der Deserialisierung.)

Es gab drei Hauptgründe für den Wechsel von Gson nach Moshi: Sicherheit, Geschwindigkeitich schlechte Lebensentscheidungen.

Sicherheit – Gson versteht die Nullsicherheit von Kotlin nicht und platziert gerne Nullwerte in Eigenschaften ungleich Null. Auch die Standardwerte funktionieren nur gelegentlich (je nach Designeinstellung).

Geschwindigkeit – Genügend Benchmarks (1, 2, 3) haben gezeigt, dass Moshi normalerweise schneller ist als Gson. Nachdem wir uns umgedreht hatten, richteten wir uns ein einige Benchmarks um zu sehen, wie das Parsing in der realen Welt in unserer App verglichen wird, und wir haben eine 2x-3,5x-Beschleunigung gesehen:

Schlechte Lebensentscheidungen – Anstatt Gson zu verwenden, um JSON in einfache Modelle zu zerlegen, schrieben wir aufwändige, verwirrende, zerbrechliche benutzerdefinierte Deserialisierer, die zu viel Logik enthielten. Refactoring gab uns die Möglichkeit, diesen Architekturfehler zu korrigieren.


Warum wir uns für Moshi im Vergleich zu Wettbewerbern entschieden haben (z. Kotlin-Serialisierung), vertrauen wir im Allgemeinen Square-Bibliotheken, wir haben Moshi in der Vergangenheit für Projekte (sowohl bei der Arbeit als auch zu Hause) verwendet und fanden, dass es gut funktioniert. Wir haben keine eingehende Untersuchung von Alternativen durchgeführt.

Der erste Schritt bestand darin, sicherzustellen, dass wir Funktionsflags verwenden können, um zwischen der Verwendung unserer alten Gson-Implementierung und der neuen Moshi-Implementierung umzuschalten. Ich schrieb ein JsonInterop eine Klasse, die basierend auf dem Flag entweder alle JSON-Antworten mit Gson oder Moshi analysiert.

(Ich habe mich entschieden, die Verwendung von Tools wie z moshi-gson-interop weil ich testen wollte, ob das Moshi-Parsing vollständig funktioniert. Wenn Sie lieber gleichzeitig eine Mischung aus Gson und Moshi haben möchten, wäre diese Bibliothek nützlich.)

Gson bietet Ihnen die Möglichkeit, den verwendeten Standardschlüsselnamen zu überschreiben @SerializedName. Mit Moshi können Sie dasselbe tun @Json. Es ist alles gut und gut, aber es schien wirklich einfach, hier einen Fehler zu machen, wo die Assets in Gson Vs unter verschiedenen Namen analysiert werden. Moshi.

Also habe ich einige Unit-Tests geschrieben, die bestätigen würden, dass unsere generierten Moshi-Adapter das gleiche Ergebnis haben wie Gsons Parsing. Ich habe besonders getestet …

  • … dass Moshi für jede Klasse, die wir deserialisieren wollten, einen Adapter (nicht unbedingt den richtigen!) generieren kann. (Wenn er das nicht könnte, würde Moshi eine Ausnahme auslösen.)
  • … womit jedes Feld gekennzeichnet ist @SerializedName ist ebenfalls mit gekennzeichnet @Json (mit dem gleichen Schlüssel).

Zwischen diesen beiden Überprüfungen war es leicht zu finden, wenn ich den Fehler gemacht habe, unsere Klassen in späteren Schritten zu aktualisieren.

(Ich kann die Quelle hier nicht angeben, aber wir haben im Grunde verwendet Guavas ClassPath um alle unsere Klassen zu sammeln und sie dann auf Probleme zu scannen.)

Mit Gson können Sie generische JSON-Bäume analysieren JsonElement (und Freunde). Wir fanden dies in einigen Kontexten nützlich, wie z. B. beim Parsen von Socket-Updates (wo wir bis zu einer anfänglichen Verarbeitung nicht genau wissen würden, wie das Antwortmodell zu parsen ist).

Offensichtlich wird Moshi mit der Verwendung von Gsons Uhren nicht zufrieden sein, also sind wir dazu übergegangen, sie zu verwenden Map<String, Any?> (und manchmal List<Map<String, Any?>>) für generische Datenbäume. Sowohl Gson als auch Moshi können dies aufschlüsseln:

fun <T> fromJson(map: Map<String, Any?>?, clz: Class<T>): T? {
  return if (USE_MOSHI) {
    moshi.adapter(clz).fromJsonValue(map)
  }
  else {
    gson.fromJson(gson.toJsonTree(map), clz)
  }
}

Darüber hinaus ist Gson freundlich, die Straße zu analysieren Leser, aber Moshi nicht. Ich habe es mit gefunden BufferedSource war eine gute Alternative, da es für alten Gson-Code in Reader konvertiert werden kann.

Die einfachsten Adapter für Moshi sind die, die Sie gerade getroffen haben @JsonClass auf sie und nennen Sie es eines Tages. Leider hatten wir, wie ich bereits erwähnte, eine Menge unglücklicher angepasster Deserialisierungslogik in unserem Gson-Parser.

Es ist ziemlich einfach Schreiben Sie einen benutzerdefinierten Moshi-Adaptersondern weil es so war so viel benutzerdefinierte Logik in unseren Deserialisierern, würde das Schreiben eines einzigen Adapters das nicht brechen. Am Ende mussten wir Interspace-Modelle erstellen, um das rohe JSON zu parsen, und uns dann an die Modelle anpassen, an die wir gewöhnt waren.

Stellen Sie sich, um ein konkretes Beispiel zu geben, vor, wir hätten a data class Foo(val count: Int)aber das eigentliche JSON, das wir bekommen, hat die Form von:

{
  "data": { 
    "count": 5
  }
}

Bei Gson konnten wir nur manuell auf den Baum schauen und die Nummer abrufen data Thema, aber wir haben festgestellt, dass darin der Wahnsinn liegt. Wir würden lieber einfach mit einfachen POJOs parsen, aber wir wollen trotzdem irgendwann Foo rausschmeißen (damit wir nicht unsere gesamte Codebasis ändern müssen).

Um dieses Problem zu lösen, würden wir neue Modelle erstellen und diese in einem benutzerdefinierten Adapter wie diesem verwenden:

@JsonClass(generateAdapter = true) data class JsonFoo(val data: JsonData)

@JsonClass(generateAdapter = true) data class JsonData(val count: Int)

object FooAdapter {
  @FromJson
  fun fromJson(json: JsonFoo): Foo {
    return Foo(count = json.data.count)
  }
}

Voila! Jetzt kann der Parser immer noch Foo auswerfen, aber wir verwenden einfache POJOs, um unsere Daten zu modellieren. Es ist einfacher zu interpretieren und zu testen.

Erinnern Sie sich, wie ich sagte, dass Gson gerne Nullwerte in Nicht-Null-Modelle analysieren würde? Es stellte sich heraus, dass wir uns (leider) an allen möglichen Stellen auf dieses Verhalten verlassen haben. Insbesondere Trello-Sockets geben oft Teilmodelle zurück – während wir also normalerweise erwarten würden, dass eine Karte mit einem Namen zurückkommt, wird dies in einigen Fällen nicht der Fall sein.

Dies bedeutete, dass wir unsere Stürze in Fällen überwachen mussten, in denen Moshi (aufgrund des Nullwerts) explodieren würde, wenn Gson wie eine Hülle glücklich wäre. Hier glänzen die Funktionsflags wirklich, da Sie den fehlerhaften Parser nicht ahnungslosen Produktionsbenutzern aufdrängen wollen!

Nachdem ich ein Dutzend dieser Fehler behoben habe, habe ich das Gefühl, dass ich viel Wertschätzung für Nicht-JSON-Technologien mit gut definierten Schemas wie Protokollpuffer. Es gibt viele Fehler, auf die ich gestoßen bin, die einfach nicht passiert wären, wenn wir einen Vertrag zwischen dem Server und dem Client gehabt hätten.