Это следующая запись в блоге из серии Ежи читает код. Первые двое были про Окио, эта про Моши.

Эту библиотеку также можно было бы назвать 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 поиск и построение.