Moshi для Kotlin: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Moshi для Kotlin: полное руководство с примерами

1. Что это и зачем нужна

Moshi — это современная библиотека для сериализации и десериализации JSON, разработанная компанией Square. Она написана на Kotlin и Java, но в первую очередь ориентирована на Kotlin-разработчиков. Moshi предоставляет мощный, безопасный и производительный способ преобразования объектов в JSON и обратно, с минимальным количеством шаблонного кода.

Зачем нужна Moshi? В современной разработке приложений работа с JSON является неотъемлемой частью: обмен данными с REST API, хранение конфигураций, передача сообщений между компонентами. Moshi решает несколько ключевых задач: автоматическое преобразование классов Kotlin в JSON (и наоборот) без необходимости писать парсеры вручную; строгая типизация, что предотвращает ошибки времени выполнения; поддержка неизменяемых объектов (data class) и sealed классов; возможность кастомизации через адаптеры. По сравнению с Gson или Jackson, Moshi предлагает более чистый API, лучшую интеграцию с Kotlin (например, поддержка null-безопасности и значений по умолчанию) и меньший размер библиотеки.

Moshi особенно полезна в проектах, где важна производительность и безопасность типов. Она активно используется в Android-разработке (в том числе в комбинации с Retrofit и OkHttp), в серверных приложениях на Ktor, а также в любых Kotlin-проектах, где требуется работа с JSON. Библиотека поддерживает рефлексию (через модуль moshi-kotlin) и кодогенерацию (через moshi-kotlin-codegen), что позволяет выбирать между удобством и производительностью.

2. Установка

Moshi распространяется через Maven Central. Для установки добавьте зависимости в файл build.gradle.kts (Gradle) или pom.xml (Maven).

Gradle (Kotlin DSL)

// build.gradle.kts
dependencies {
    // Основная библиотека
    implementation("com.squareup.moshi:moshi:1.15.1")
    
    // Модуль для Kotlin (рефлексия)
    implementation("com.squareup.moshi:moshi-kotlin:1.15.1")
    
    // Кодогенерация (опционально, для повышения производительности)
    ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.1")
    // или kapt("com.squareup.moshi:moshi-kotlin-codegen:1.15.1")
}

Gradle (Groovy)

// build.gradle
dependencies {
    implementation 'com.squareup.moshi:moshi:1.15.1'
    implementation 'com.squareup.moshi:moshi-kotlin:1.15.1'
    ksp 'com.squareup.moshi:moshi-kotlin-codegen:1.15.1'
}

Maven

<dependency>
    <groupId>com.squareup.moshi</groupId>
    <artifactId>moshi</artifactId>
    <version>1.15.1</version>
</dependency>
<dependency>
    <groupId>com.squareup.moshi</groupId>
    <artifactId>moshi-kotlin</artifactId>
    <version>1.15.1</version>
</dependency>

Важно: Если вы используете кодогенерацию, не забудьте подключить плагин KSP (Kotlin Symbol Processing) или KAPT. Для KSP добавьте в plugins:

plugins {
    id("com.google.devtools.ksp") version "1.9.22-1.0.17"
}

3. Быстрый старт

Минимальный рабочий пример демонстрирует базовую сериализацию и десериализацию.

import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory

// 1. Определяем data class
data class User(
    val name: String,
    val age: Int,
    val email: String?
)

fun main() {
    // 2. Создаём экземпляр Moshi с фабрикой для Kotlin
    val moshi = Moshi.Builder()
        .addLast(KotlinJsonAdapterFactory())
        .build()
    
    // 3. Получаем адаптер для класса User
    val adapter = moshi.adapter(User::class.java)
    
    // 4. Сериализация: объект -> JSON
    val user = User("Alice", 30, "alice@example.com")
    val json = adapter.toJson(user)
    println("JSON: $json")
    // Вывод: JSON: {"name":"Alice","age":30,"email":"alice@example.com"}
    
    // 5. Десериализация: JSON -> объект
    val jsonString = """{"name":"Bob","age":25,"email":null}"""
    val parsedUser = adapter.fromJson(jsonString)
    println("Parsed user: $parsedUser")
    // Вывод: Parsed user: User(name=Bob, age=25, email=null)
}

4. Основные методы, функции и классы

4.1. Moshi.Builder() — создание и настройка экземпляра Moshi

Сигнатура: class Moshi.Builder

Что делает: Позволяет создать экземпляр Moshi с кастомными адаптерами, фабриками и настройками.

val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory()) // Добавляем фабрику для Kotlin
    .add(MyCustomAdapter())          // Добавляем кастомный адаптер
    .build()

4.2. Moshi.adapter() — получение адаптера для класса

Сигнатура: fun <T> adapter(type: Class<T>): JsonAdapter<T>

Что делает: Возвращает адаптер для указанного класса, который можно использовать для сериализации/десериализации.

val adapter = moshi.adapter(User::class.java)
val json = adapter.toJson(user)
val user = adapter.fromJson(json)

4.3. JsonAdapter.toJson() — сериализация объекта в JSON

Сигнатура: fun toJson(value: T): String

Что делает: Преобразует объект в JSON-строку.

val user = User("Charlie", 35, "charlie@test.com")
val json = adapter.toJson(user)
// Результат: {"name":"Charlie","age":35,"email":"charlie@test.com"}

4.4. JsonAdapter.fromJson() — десериализация JSON в объект

Сигнатура: fun fromJson(json: String): T?

Что делает: Преобразует JSON-строку в объект указанного типа. Возвращает null, если JSON некорректен.

val json = """{"name":"Diana","age":28,"email":"diana@test.com"}"""
val user = adapter.fromJson(json)
println(user?.name) // Вывод: Diana

4.5. @Json аннотация — кастомизация имени поля

Сигнатура: @Json(name = "custom_name")

Что делает: Позволяет задать другое имя для поля в JSON, отличное от имени свойства в классе.

data class Product(
    @Json(name = "product_id") val id: Int,
    val name: String
)

// JSON: {"product_id":1,"name":"Laptop"}
val product = moshi.adapter(Product::class.java).fromJson("""{"product_id":1,"name":"Laptop"}""")
println(product?.id) // 1

4.6. @Transient аннотация — исключение поля из JSON

Сигнатура: @Transient

Что делает: Помечает поле, которое не должно участвовать в сериализации/десериализации.

data class Config(
    val url: String,
    @Transient val password: String = "default"
)

val config = Config("https://api.example.com", "secret123")
val json = moshi.adapter(Config::class.java).toJson(config)
// Результат: {"url":"https://api.example.com"} (password отсутствует)

4.7. JsonReader — низкоуровневое чтение JSON

Сигнатура: class JsonReader

Что делает: Позволяет вручную парсить JSON, читая токены (beginObject, name, value, endObject и т.д.). Полезен для сложных или нестандартных форматов.

val json = """{"name":"Eve","scores":[10,20,30]}"""
val reader = JsonReader.of(json.toBuffer())
reader.beginObject()
var name = ""
val scores = mutableListOf<Int>()
while (reader.hasNext()) {
    when (reader.nextName()) {
        "name" -> name = reader.nextString()
        "scores" -> {
            reader.beginArray()
            while (reader.hasNext()) scores.add(reader.nextInt())
            reader.endArray()
        }
    }
}
reader.endObject()
println("Name: $name, Scores: $scores")

4.8. JsonWriter — низкоуровневая запись JSON

Сигнатура: class JsonWriter

Что делает: Позволяет вручную создавать JSON, управляя структурой через beginObject, name, value, endObject и т.д.

val buffer = Buffer()
val writer = JsonWriter.of(buffer)
writer.beginObject()
writer.name("status").value("active")
writer.name("count").value(42)
writer.endObject()
writer.close()
println(buffer.readUtf8()) // {"status":"active","count":42}

4.9. JsonAdapter.nullSafe() — обработка null значений

Сигнатура: fun nullSafe(): JsonAdapter<T>

Что делает: Оборачивает адаптер, чтобы он корректно обрабатывал null значения (возвращает null вместо выброса исключения).

val safeAdapter = adapter.nullSafe()
val result = safeAdapter.fromJson("null")
println(result) // null (без исключения)

4.10. Moshi.Builder.add() — добавление кастомного адаптера

Сигнатура: fun add(adapter: Any): Builder

Что делает: Регистрирует кастомный адаптер или фабрику адаптеров для обработки специфических типов.

class LocalDateAdapter : JsonAdapter<LocalDate>() {
    @FromJson
    override fun fromJson(reader: JsonReader): LocalDate? {
        return reader.nextString()?.let { LocalDate.parse(it) }
    }
    
    @ToJson
    override fun toJson(writer: JsonWriter, value: LocalDate?) {
        value?.let { writer.value(it.toString()) }
    }
}

val moshi = Moshi.Builder()
    .add(LocalDateAdapter())
    .addLast(KotlinJsonAdapterFactory())
    .build()

5. Реальный практический пример

Рассмотрим полный пример приложения для работы с API погоды. Мы создадим модель данных, настроим Moshi с кастомными адаптерами и выполним сериализацию/десериализацию.

import com.squareup.moshi.*
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import okio.Buffer
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

// --- Модели данных ---
data class WeatherResponse(
    val location: Location,
    val current: CurrentWeather,
    val forecast: List<ForecastDay>?
)

data class Location(
    val name: String,
    val country: String,
    @Json(name = "lat") val latitude: Double,
    @Json(name = "lon") val longitude: Double
)

data class CurrentWeather(
    @Json(name = "temp_c") val temperatureCelsius: Double,
    @Json(name = "condition") val condition: WeatherCondition,
    @Json(name = "last_updated") val lastUpdated: LocalDateTime
)

data class WeatherCondition(
    val text: String,
    val icon: String,
    val code: Int
)

data class ForecastDay(
    val date: LocalDateTime,
    @Json(name = "day") val day: DayForecast
)

data class DayForecast(
    @Json(name = "maxtemp_c") val maxTemp: Double,
    @Json(name = "mintemp_c") val minTemp: Double,
    @Json(name = "condition") val condition: WeatherCondition
)

// --- Кастомный адаптер для LocalDateTime ---
class LocalDateTimeAdapter : JsonAdapter<LocalDateTime>() {
    private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
    
    @FromJson
    override fun fromJson(reader: JsonReader): LocalDateTime? {
        val dateStr = reader.nextString() ?: return null
        return LocalDateTime.parse(dateStr, formatter)
    }
    
    @ToJson
    override fun toJson(writer: JsonWriter, value: LocalDateTime?) {
        writer.value(value?.format(formatter))
    }
}

// --- Ада                    

Рекомендаций