Котлин + Стрелка + Гсон = Нет? - PullRequest
0 голосов
/ 02 ноября 2018

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

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 Ответ

0 голосов
/ 03 ноября 2018

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

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

Это означает, что Gson не знает, как создать класс 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)]
...