Котлин + Стрелка + Гсон = Нет?

У меня есть модель в Kotlin простой библиотеки Книг и Заемщиков, где Книга проверена, если у нее есть Заемщик. Я использую Arrow Option для кодирования отсутствия / присутствия Заемщика:

data class Borrower(val name: Name, val maxBooks: MaxBooks)
data class Book(val title: String, val author: String, val borrower: Option<Borrower> = None)

У меня возникли проблемы с сериализацией / десериализацией этих объектов в / из JSON в Gson - в частности, представление Option<Borrower> в JSON null внутри книги:

[
  {
    "title": "Book100",
    "author": "Author100",
    "borrower": {
      "name": "Borrower100",
      "maxBooks": 100
    }
  },
  {
    "title": "Book200",
    "author": "Author200",
    "borrower": null
  }
]

Мой десериализованный код:

fun jsonStringToBooks(jsonString: String): List<Book> {
    val gson = Gson()
    return try {
        gson.fromJson(jsonString, object : TypeToken<List<Book>>() {}.type)
    } catch (e: Exception) {
        emptyList()
    }
}

Я получаю пустой список. Почти идентичный jsonStringToBorrowers работает отлично.

Может ли кто-нибудь указать мне правильное направление?

Будет ли использовать другую библиотеку JSON, как kotlinx.serialization или же Klaxon быть лучшей идеей и как они делают null <-> None вещь?

Спасибо!

1 ответ

Решение

Эта проблема немного скрыта тем, что вы не регистрируете исключение перед возвратом пустого списка. Если бы вы зарегистрировали это исключение, вы бы получили это:

java.lang.RuntimeException: не удалось вызвать закрытый arrow.core.Option() без аргументов

Это означает, что Гсон не знает, как создать Option класс, потому что у него нет открытого пустого конструктора. В самом деле, Option является запечатанным классом ( следовательно, абстрактным), имеющим 2 конкретных дочерних класса: Some а также None, Для того, чтобы получить экземпляр Option Вы должны использовать один из заводских методов, как Option.just(xxx) или же Option.empty() среди других.

Теперь, чтобы исправить ваш код, вы должны сказать Gson, как десериализовать Option учебный класс. Для этого вам нужно зарегистрировать адаптер типа gson объект.

Возможная реализация заключается в следующем:

class OptionTypeAdapter<E>(private val adapter: TypeAdapter<E>) : TypeAdapter<Option<E>>() {

    @Throws(IOException::class)
    override fun write(out: JsonWriter, value: Option<E>) {
        when (value) {
            is Some -> adapter.write(out, value.t)
            is None -> out.nullValue()
        }
    }

    @Throws(IOException::class)
    override fun read(input: JsonReader): Option<E> {
        val peek = input.peek()
        return if (peek != JsonToken.NULL) {
            Option.just(adapter.read(input))
        } else {
            input.nextNull()
            Option.empty()
        }
    }

    companion object {

        fun getFactory() = object : TypeAdapterFactory {
            override fun <T> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
                val rawType = type.rawType as Class<*>
                if (rawType != Option::class.java) {
                    return null
                }
                val parameterizedType = type.type as ParameterizedType
                val actualType = parameterizedType.actualTypeArguments[0]
                val adapter = gson.getAdapter(TypeToken.get(actualType))
                return OptionTypeAdapter(adapter) as TypeAdapter<T>
            }
        }
    }

}

Вы можете использовать его следующим образом:

fun main(args: Array<String>) {
    val gson = GsonBuilder()
        .registerTypeAdapterFactory(OptionTypeAdapter.getFactory())
        .create()
    val result: List<Book> = try {
        gson.fromJson(json, TypeToken.getParameterized(List::class.java, Book::class.java).type)
    } catch (e: Exception) {
        e.printStackTrace()
        emptyList()
    }

    println(result)
}

Этот код выводит:

[Book(title=Book100, author=Author100, borrower=Some(Borrower(name=Borrower100, maxBooks=100))), Book(title=Book200, author=Author200, borrower=None)]
Другие вопросы по тегам