Avatar ·

Как использовать TypeToken с дженериками в Gson в Kotlin (решение ошибки com.google.gson.internal.LinkedTreeMap)

Всем привет! Я уже несколько дней бьюсь с проблемой десериализации JSON в Kotlin, когда используются дженерики. У меня есть функция, которая принимает тип класса и возвращает распарсенный объект. В Java я бы написал что-то вроде `new TypeToken>(){}.getType()`, но в Kotlin это не работает из-за erased generics. Вот упрощённый пример: ```kotlin data class User(val name: String, val age: Int) fun parseList(json: String, clazz: Class): List { val type = TypeToken.getParameterized(List::class.java, clazz).type return Gson().fromJson(json, type) } // Использование val json = """[{"name":"John","age":30}]""" val users = parseList(json, User::class.java) println(users[0].name) // Ошибка: java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to User ``` Я перепробовал много вариантов: пытался передать `TypeToken`, использовать `reified`, но всё равно получаю `LinkedTreeMap` вместо моего типа. Понимаю, что это связано с тем, что JVM стирает информацию о дженериках во время выполнения, но как правильно обойти это в Kotlin? Мне нужно решение, которое будет работать для любого вложенного дженерика, а не только для `List`. Например, для `Map` или `Result>`. Подскажите, как правильно использовать TypeToken с Gson в Kotlin? Спасибо! <<>>

Проблема: erased generics в JVM

Вы абсолютно правы: корень проблемы — стирание типов (type erasure) в Java/Kotlin. Во время выполнения JVM не знает, что List<User> — это список именно пользователей, для неё это просто List. Поэтому Gson не может создать экземпляры User и возвращает LinkedTreeMap (словарь), который является внутренним представлением JSON-объекта.

Решение: reified + inline + TypeToken

В Kotlin есть мощный механизм — reified type parameters. Он работает только с inline-функциями и позволяет сохранить информацию о типе во время компиляции. Вот правильный подход:

import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type

inline fun <reified T> parseList(json: String): List<T> {
    // TypeToken создаёт объект, который "запоминает" тип T
    val type = object : TypeToken<List<T>>() {}.type
    return Gson().fromJson(json, type)
}

data class User(val name: String, val age: Int)

fun main() {
    val json = """[{"name":"John","age":30}]"""
    val users: List<User> = parseList(json) // Тип выводится автоматически
    println(users[0].name) // John — всё работает!
}

Универсальное решение для любых дженериков

Часто нужно десериализовать не только список, но и другие обобщённые типы (например, Map<String, User> или кастомные классы). Создайте вспомогательную функцию:

inline fun <reified T> fromJson(json: String): T {
    val type = object : TypeToken<T>() {}.type
    return Gson().fromJson(json, type)
}

// Примеры использования:
fun main() {
    val jsonList = """[{"name":"Alice"}]"""
    val jsonMap = """{"key1":{"name":"Bob"}}"""
    val jsonCustom = """{"data":[{"name":"Charlie"}],"error":null}"""

    val users: List<User> = fromJson(jsonList)
    val userMap: Map<String, User> = fromJson(jsonMap)
    val result: Result<List<User>> = fromJson(jsonCustom)

    println(users[0].name)        // Alice
    println(userMap["key1"]?.name) // Bob
    println(result.data?.first()?.name) // Charlie
}

// Вспомогательный класс для примера
data class Result<T>(val data: T?, val error: String?)

Почему это работает?

  • inline: функция встраивается в место вызова, поэтому компилятор "знает" конкретный тип T.
  • reified: позволяет использовать T внутри функции, хотя обычно дженерики стираются.
  • object : TypeToken<T>() {}: создаёт анонимный класс, который наследуется от TypeToken. Благодаря этому Gson получает полную информацию о типе (включая вложенные дженерики) через рефлексию.

Важные замечания

  • Не используйте TypeToken.getParameterized() — это устаревший метод, который может работать некорректно с Kotlin.
  • Если вы используете ProGuard/R8, добавьте правила для сохранения TypeToken:
-keep class com.google.gson.reflect.TypeToken { *; }
-keep class * extends com.google.gson.reflect.TypeToken
  • Для Android-проектов с kotlinx.serialization рассмотрите альтернативы Gson, но если вы привязаны к Gson — описанный метод надёжен и проверен.

Этот подход — стандартный и рекомендуемый способ работы с дженериками в Gson для Kotlin. Он решает проблему LinkedTreeMap раз и навсегда.

<<>>

Альтернатива: использование TypeToken через вспомогательный класс

Если вы по каким-то причинам не хотите использовать reified (например, ваш код не inline), можно создать вспомогательный класс-обёртку, который явно хранит тип:

import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type

class JsonParser<T>(private val type: Type) {
    fun parse(json: String): T = Gson().fromJson(json, type)

    companion object {
        inline fun <reified T> create(): JsonParser<T> {
            return JsonParser(object : TypeToken<T>() {}.type)
        }
    }
}

// Использование:
data class User(val name: String)

fun main() {
    val json = """{"name":"John"}"""
    val parser = JsonParser.create<User>()
    val user = parser.parse(json)
    println(user.name) // John
}

Минусы этого подхода

  • Требуется создать экземпляр парсера — лишняя строчка кода.
  • Не так лаконично, как inline reified.

Дополнительный совет: избегайте raw-типов

Никогда не используйте Gson().fromJson(json, List::class.java) — это всегда приведёт к LinkedTreeMap. Всегда передавайте полный тип с помощью TypeToken или reified.

Для большинства случаев первый ответ (с inline reified fromJson) является лучшим решением — он компактный, универсальный и не требует создания дополнительных классов.