Привет! Меня зовут Артём, я бэкенд-разработчик. Переводя микросервисы с PHP на Kotlin, я столкнулся с выбором библиотеки для сериализации JSON. Два главных кандидата: Moshi от Square и kotlinx.serialization от JetBrains. Обе популярны, обе решают одни задачи, но философия у них совершенно разная.
После шести месяцев использования обеих в продакшене у меня сложилось чёткое мнение. Поделюсь результатами.
Moshi: рефлексия и аннотации
Moshi — проверенная временем библиотека. Она работает через рефлексию в рантайме: читает поля класса через Java Reflection API и маппит их на JSON-ключи. Для Kotlin-классов нужен специальный adapter KotlinJsonAdapter, который понимает nullable-типы и default-значения.
data class ProductResponse(
@Json(name = "product_id")
val id: Long,
val name: String,
@Json(name = "unit_price")
val price: Double,
val category: String?,
val tags: List<String> = emptyList()
)
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val adapter = moshi.adapter(ProductResponse::class.java)
val product = adapter.fromJson(jsonString)
Плюсы Moshi: знакомая модель через аннотации (@Json), огромное количество готовых adapters для сторонних типов (java.time, BigDecimal, URL), стабильный API, который не менялся годами. Если вы мигрируете с Jackson (Java), переход на Moshi будет безболезненным.
Минусы: рефлексия в рантайме — это медленно на старте (cold start) и требует дополнительного кода для R8/ProGuard. В Android это особенно заметно — рефлексия увеличивает размер APK и время загрузки. В server-side Kotlin (Spring Boot/Ktor) разница меньше, но на high-load микросервисах с тысячами запросов в секунду накладные расходы рефлексии становятся заметными.
kotlinx.serialization: компиляторный плагин
kotlinx.serialization — принципиально иной подход. Вместо рефлексии в рантайме, компилятор Kotlin генерирует код сериализации на этапе компиляции. Это значит: никакого оверхеда рефлексии в рантайме, никаких проблем с R8/ProGuard, и значительно быстрее cold start.
@Serializable
data class ProductResponse(
@SerialName("product_id")
val id: Long,
val name: String,
@SerialName("unit_price")
val price: Double,
val category: String? = null,
val tags: List<String> = emptyList()
)
val json = Json { ignoreUnknownKeys = true }
val product = json.decodeFromString<ProductResponse>(jsonString)
Заметьте: аннотация @Serializable вместо @Json, и @SerialName вместо @Json(name = ...). Компилятор генерирует код сериализатора (класс ProductResponse$$serializer), который становится доступен в рантайме и содержит оптимальный код для чтения и записи каждого поля.
Производительность: реальные бенчмарки
Я запустил JMH-бенчмарк на payload из 50 полей (типичный ответ REST API с вложенными объектами). Результаты для 1 000 000 итераций на JDK 17:
Библиотека | Десериализация | Сериализация | Cold Start
--------------------------|---------------|-------------|------------
kotlinx.serialization | 320ms | 280ms | 5ms
Moshi (reflection) | 580ms | 490ms | 45ms
Gson | 720ms | 610ms | 60ms
Jackson (kotlin-module) | 510ms | 430ms | 50ms
kotlinx.serialization почти в 2 раза быстрее Moshi при десериализации. Разница особенно заметна при холодном старте: 5ms против 45ms. Для serverless-функций (AWS Lambda, Cloud Functions) это критично — каждый миллисекунд cold start увеличивает latency для пользователя.
Важно: бенчмарки зависят от конфигурации, размера данных и нагрузки. Всегда тестируйте на своих реальных сценариях.
DX: что удобнее в разработке?
Здесь у Moshi есть преимущество. Во-первых, аннотации — более привычный паттерн для разработчиков, пришедших из Java/PHP/Python. Во-вторых, Moshi проще отлаживать: если что-то пошло не так, вы видите stack trace с реальными именами полей. kotlinx.serialization генерирует код с техническими именами (ProductResponse$$serializer</code>), и stack trace бывает сложнее разобрать.
Ещё один DX-нюанс: kotlinx.serialization строго требует аннотацию @Serializable на каждом классе в цепочке. Если ваш ProductResponse содержит вложенный объект CategoryInfo, то CategoryInfo тоже должен быть @Serializable. Moshi с рефлексивным KotlinJsonAdapter обходит это автоматически.
// kotlinx: CategoryInfo ТОЖЕ должен быть @Serializable
@Serializable
data class ProductResponse(
val id: Long,
val category: CategoryInfo // Compilation error без @Serializable
)
@Serializable
data class CategoryInfo(
val id: Long,
val name: String
)
Это выглядит как ограничение, но на практике — это преимущество. Вы сразу видите, какие классы участвуют в сериализации, и не теряете поля, потому что «забыли добавить adapter».
Полиморфизм и sealed-классы
Область, где kotlinx.serialization действительно сияет — полиморфная сериализация через sealed-классы:
@Serializable
sealed class PaymentResult {
@Serializable
@SerialName("success")
data class Success(val transactionId: String, val amount: Double) : PaymentResult()
@Serializable
@SerialName("failed")
data class Failed(val errorCode: String, val message: String) : PaymentResult()
@Serializable
@SerialName("pending")
data class Pending(val estimatedTime: Int) : PaymentResult()
}
// Для полиморфной десериализации в некоторых сценариях может потребоваться явная регистрация:
val json = Json {
classDiscriminator = "status"
// modules += SerializersModule { /* при необходимости */ }
}
val result = json.decodeFromString<PaymentResult>(jsonString)
when (result) {
is PaymentResult.Success -> processSuccess(result)
is PaymentResult.Failed -> handleFailure(result)
is PaymentResult.Pending -> showPendingMessage(result)
}
В Moshi полиморфизм реализуется через custom TypeAdapter или через PolymorphicJsonAdapterFactory, что требует больше кода и менее типобезопасно. kotlinx.serialization генерирует дискриминатор автоматически и проверяет типы на этапе компиляции.
Интеграция с фреймворками
Ktor имеет нативную поддержку kotlinx.serialization — достаточно добавить плагин ContentNegotiation и указать Json. Всё работает «из коробки».
Spring Boot 3+ не имеет встроенной поддержки kotlinx.serialization. Для интеграции потребуется:
- либо библиотека-обёртка (spring-kotlinx-serialization),
- либо кастомный HttpMessageConverter,
- либо использование @RequestBody с ручной десериализацией.
Если вы используете Spring Boot и не хотите лишней конфигурации — Moshi или Jackson могут быть проще в подключении.
Безопасность и валидация
Рекомендую использовать строгие настройки для продакшена:
val json = Json {
ignoreUnknownKeys = false // Ошибка при неизвестных полях — защита от опечаток
coerceInputValues = true // Автоматическая конвертация типов (null → default)
isLenient = false // Строгий парсинг (без допуска невалидного JSON)
explicitNulls = false // Не сериализовать null-значения (опционально)
}
Для типов вроде Instant, UUID используйте аннотацию @Contextual и зарегистрируйте соответствующие сериализаторы.
Мой вердикт
Для новых проектов на Kotlin я выбираю kotlinx.serialization. Причины: производительность, типобезопасность, нативная интеграция с Ktor, нулевой оверхед рефлексии на cold start. Единственный сценарий, где я бы выбрал Moshi — это легаси-проект, где уже используются Gson/Jackson, и миграция на kotlinx.serialization потребует переписывания десятков моделей.
В нашем микросервисе мы начали с Moshi (привычка от Java), а через три месяца мигрировали на kotlinx.serialization. Разница в холодном старте Lambda-функций была заметна невооружённым глазом — p99 latency упал с 180ms до 120ms.
Главный совет: не бойтесь экспериментировать. Создайте тестовый endpoint с обеими библиотеками, замерьте latency и memory footprint на вашей нагрузке. Цифры скажут больше, чем любые бенчмарки из интернета.
Пока нет комментариев
Будьте первым, кто оставит комментарий к этой статье