Это следующая запись в блоге из серии Ежи читает код. Первые двое были про Окио, эта про Моши.
Эту библиотеку также можно было бы назвать OkJson, потому что это библиотека JVM JSON от Square. В некотором смысле это также v3 библиотеки Gson - основной разработчик Moshi также участвовал в проекте Gson, и сходство между этими библиотеками, как на уровне API, так и на уровне кода, легко заметить.
Большая фотография
Точкой входа в API является объект Moshi
. Вы создаете его, передавая различные объекты конфигурации в Moshi.Builder
, а затем вызывая build()
method. Затем вы можете попросить Moshi
предоставить JsonAdapter<T>
для данного типа JVM. JsonAdapter<T>
предоставляет методы для преобразования объекта Java типа T
в JSON String
или «значение JSON» (структура JSON, представленная как примитивы JVM и базовые типы коллекций) и наоборот.
В этом посте основное внимание уделяется нижней части этого стека: a JsonAdapter
и родственным классам.
JsonAdapter
Этот класс предоставляет высокоуровневую абстракцию обработки JSON. Вам нужно указать, как ваш объект разлагается на различные примитивы JSON и как собрать его из этих отдельных частей:
@CheckReturnValue
public
abstract
@Nullable
T fromJson(JsonReader reader) throws
IOException;
public
abstract
void
toJson(JsonWriter writer, @Nullable
T value) throws
IOException;
Фактическая низкоуровневая кодировка JSON делегируется классам JsonWriter
и JsonReader
. Эти интерфейсы (или, точнее, общедоступные абстрактные классы) имеют две внутренние реализации: одна работает со строками JSON, другая - со значениями JSON. Они инкапсулируют мельчайшие детали кодирования и декодирования JSON и предоставляют все обычные методы работы с JSON, такие как отметка начала и конца объекта или массива.
JsonAdapter
также предоставляет несколько методов для настройки некоторых высокоуровневых характеристик адаптера: как обрабатываются нули, выходные отступы и т. Д. Это реализовано с использованием шаблона декоратора.
Что делает эту часть кодовой базы интересной, так это пара, казалось бы, странных методов на JsonReader
и JsonWriter
, которые оказались очень полезными при работе с некоторыми распространенными шаблонами JSON.
Полиморфный JSON
Допустим, у вас есть такой запечатанный класс:
sealed class
UserId { data class
Username(val name: String) : UserId() data class
Email(val email: String) : UserId() }
Когда вы сериализуете UserId
в JSON, вам необходимо закодировать информацию о типе, чтобы позже вы знали, следует ли десериализовать ее в Username
или Email
. Обычно в представление объекта вы добавляете поле типа type
:
{
"type": "email",
"email": "[email protected]"
}
Написание такого JsonAdapter
может показаться тривиальным, но на самом деле это немного сложно, если вы хотите делегировать некоторую работу JsonAdapters
для каждого подкласса. Например, эта наивная реализация не сработает:
class UserIdAdapter(private val usernameAdapter: JsonAdapter<Username>, private val emailAdapter: JsonAdapter<Email>) : JsonAdapter<UserId>() { override fun toJson(writer: JsonWriter, value: UserId?) { writer.beginObject() when (value) { is Username -> { writer.name("type").value("username") usernameAdapter.toJson(writer, value) } is Email -> { writer.name("type").value("email") emailAdapter.toJson(writer, value) } } writer.endObject() } override fun fromJson(reader: JsonReader): UserId? { reader.beginObject() reader.skipName() val result = when (val type = reader.nextString()) { "username" -> usernameAdapter.fromJson(reader) "email" -> emailAdapter.fromJson(reader) else -> throw IllegalArgumentException("Unknown UserId type: $type") } reader.endObject() return result } }
toJson
попытается построить этот недействительный JSON:
{ "type": "email", { "email": "[email protected]" } }
Точно так же fromJson
будет подавляться клавишей “email”
, потому что JsonAdapter<Email>
, который мы вызываем, ожидает открывающую фигурную скобку объекта JSON. Другой проблемой здесь является предположение, что поле “type”
появится в объекте JSON перед любыми другими данными.
Moshi
дает нам инструменты для решения обеих проблем. Для десериализации мы можем использовать JsonReader.peekJson()
, который возвращает новый JsonReader
, из которого вы можете читать, не затрагивая исходный. В нашем адаптере мы можем использовать проверенный JsonReader
, чтобы определить, какой адаптер использовать, и делегировать чтение из исходного JsonReader
. Для сериализации существует _48 _ / _ 49_, которые позволяют подавлять выдачу маркеров открытия и закрытия объекта JSON или массива JSON. Вместе эти методы позволяют без особых хлопот написать такой адаптер (полная реализация такого адаптера доступна в дополнительном модуле moshi -adapters).
Обработка необработанного JSON
Иногда у вас есть String
, содержащий действительный JSON, который вам просто нужно встроить в какой-то другой документ JSON. Анализ этого String
в каком-либо другом представлении просто для того, чтобы сбросить его обратно в JSON, является расточительным, поэтому JsonWriter
предоставляет value(BufferedSource)
метод, который позволяет вам дословно поместить некоторые данные в JSON.
Продвижение между ценностями и именами
При сериализации объекта JSON для каждого поля вы сначала пишете имя, а затем значение поля. JsonWriter
API отражает это: существует отдельный метод для записи имени и набор перегруженных методов для записи значения. Этот API отлично работает, когда вы пишете свой JsonAdapters
вручную и вызываете все JsonWriter
методы напрямую, но это может вызвать проблемы, когда вам нужно делегировать вызовы JsonWriter
другому JsonAdapter
.
Например, при сериализации Map<SomeEnum, Whatever>
вы, вероятно, захотите написать такой код:
val someEnumAdapter: JsonAdapter<SomeEnum> = … val whateverAdapter: JsonAdapter<Whatever> = … fun toJson(writer: JsonWriter, value: …) { // … writer.name("mapping") writer.beginObject() for ((key, value) in value.mapping) { someEnumAdapter.toJson(writer, key) whateverAdapter.toJson(writer, value) } writer.endObject() // … }
Но, вероятно, JsonAdapter<SomeEnum>
выглядит так:
fun toJson(writer: JsonWriter, value: SomeEnum) {
writer.value(value.name)
}
Таким образом, наш цикл в первом адаптере будет пытаться записать серию значений вместо последовательности name: value
пар, нарушая некоторые внутренние JsonWriter
инварианты. Мы не можем изменить SomeEnum
адаптер для выполнения JsonWriter.name
вызовов, потому что он не сработает, если кто-то захочет сериализовать Map<String, SomeEnum>
.
Чтобы решить эту проблему, используйте метод JsonWriter.promoteValueToName()
method, который указывает JsonWriter
обрабатывать следующее значение как имя:
for ((key, value) in value.mapping) { writer.promoteValueToName() someEnumAdapter.toJson(writer, key) whateverAdapter.toJson(writer, value) }
Таким образом, у вас может быть один JsonAdapter
для каждого типа, независимо от того, как этот тип используется в других сериализованных структурах данных.
Быть в курсе
Это все на сегодня. В следующем посте я опишу процесс настройки объекта Moshi и то, как работает JsonAdapter
поиск и построение.