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))
}
}
// --- Ада