Gson — разбор jsonString с полем, содержащим пустое/нулевое значение (в Kotlin)

TL;DR

Для строки json, содержащей ...,field=,..., Gson продолжает выдавать JsonSyntaxException. Что я могу сделать?

Дело

Мне нужно общаться с третьим API, который обычно предоставляет такие данные:

{
  "fieldA": "stringData",
  "fieldB": "",
  "fieldC": ""
}

Однако в моем проекте приложения это выглядит так:

val jsonString = "{fieldA=stringData,fieldB=,fieldC=}"

Эта проблема

Я попытался использовать стандартный метод для его десериализации:

val jsonString = "{fieldA=stringData,fieldB=,fieldC=}"
val parseJson = Gson().fromJson(jsonString, JsonObject::class.java)
assertEquals(3, parseJson.size())

Но это приводит к исключению:

com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException: Unexpected value at line 1 column 28 path $.fieldB

Решения, которые не работают

Я пробовал так много решений, ни одно из них не работает. В том числе:

  • Настройте пользовательский класс данных и установите значение nullable
data class DataExample(
    val fieldA: String?,
    val fieldB: String?,
    val fieldC: String?,
)
val parseToObject = Gson().fromJson(jsonString, DataExample::class.java)
data class DataExample(
    val fieldA: JsonElement,
    val fieldB: JsonElement,
    val fieldC: JsonElement,
)
val parseToObject = Gson().fromJson(jsonString, DataExample::class.java)
class EmptyToNullDeserializer<T>: JsonDeserializer<T> {
    override fun deserialize(
        json: JsonElement, typeOfT: Type, context: JsonDeserializationContext
    ): T? {
        if (json.isJsonPrimitive) {
            json.asJsonPrimitive.also {
                if (it.isString && it.asString.isEmpty()) return null
            }
        }
        return context.deserialize(json, typeOfT)
    }
}
data class DataExample(
    @JsonAdapter(EmptyToNullDeserializer::class)
    val fieldA: String?,
    @JsonAdapter(EmptyToNullDeserializer::class)
    val fieldB: String?,
    @JsonAdapter(EmptyToNullDeserializer::class)
    val fieldC: String?,
)
val parseToObject = Gson().fromJson(jsonString, DataExample::class.java)

или используя его в GsonBuilder:

val gson = GsonBuilder()
    .registerTypeAdapter(DataExample::class.java, EmptyToNullDeserializer<String>())
    .create()
val parseToObject = gson.fromJson(jsonString, DataExample::class.java)

Что еще я могу сделать?

Как вы получили эту строку "{fieldA=stringData,fieldB=,fieldC=}"? это просто недопустимый JSON. Что вы имеете в виду под "получается читать так:". вы, должно быть, сделали какое-то преобразование к нему

Ivo Beckers 18.03.2022 12:05

Проблема не в преобразовании этой строки. Но получить правильную строку. Ответ, который вы говорите «предоставьте данные, подобные этому:», является фактической строкой jsonString, с которой вы должны работать. Вы как-то трансформировали ответ

Ivo Beckers 18.03.2022 12:19

Ваш jsonString - это не JSON, а, скорее всего, результат toString, похожий на JSON. Очевидно, вы не можете разобрать его как JSON. Пробовали ли вы просить специалистов по обслуживанию вашего сервера исправить их конечные точки?

terrorrussia-keeps-killing 18.03.2022 15:08

Да, оказывается, что указанный API вообще не был действительным JSON. Извините за мое непонимание..

Samuel T. Chou 21.03.2022 03:36
Как сделать HTTP-запрос в Javascript?
Как сделать HTTP-запрос в Javascript?
В JavaScript вы можете сделать HTTP-запрос, используя объект XMLHttpRequest или более новый API fetch. Вот пример для обоих методов:
1
4
73
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Это недопустимый JSON. Вам нужно разобрать его самостоятельно. Вероятно, эта строка сделана с использованием метода Map::toString(). Вот код для его разбора в Map<String, String>

val jsonString = "{fieldA=stringData,fieldB=,fieldC=}"

val userFieldsMap = jsonString.removeSurrounding("{", "}").split(",") // split by ","
    .mapNotNull { fieldString ->
        val keyVal = fieldString.split(" = ")
        // check if array contains exactly 2 items
        if (keyVal.size == 2) {
            keyVal[0].trim() to keyVal[1].trim()  // return@mapNotNull
        } else {
            null // return@mapNotNull
        }
    }
    .toMap()

Что делать, если значение поля содержит запятую ,? Это не сработает? Я думаю, нет необходимости пытаться адаптироваться к сломанному API.

terrorrussia-keeps-killing 18.03.2022 15:10

@user12232870 user12232870 Да, это не будет работать должным образом. Мы не знаем, действительно ли ему нужно адаптироваться к сломанному API.

Kudratillo 18.03.2022 15:43

Вы, ребята, правы, данные обратно не являются допустимым JSON, и я не должен адаптироваться к нему. Тем не менее, спасибо @frc129 за попытку исправить строку.

Samuel T. Chou 21.03.2022 03:48

Оказывается, как сказал @frc129 и многие другие, это недопустимый JSON.

Однако правда в том, что Gson обрабатывает больше ситуаций, чем должен быть JSON, как показано ниже:

val jsonString = "{fieldA=stringData,fieldB=s2,fieldC=s3}"
val parseJson = Gson().fromJson(jsonString, JsonObject::class.java)
// This will NOT throw exception, even the jsonString here is not actually a JSON string.
assertEquals(3, parseJson.size())
assertEquals("stringData", parseJson["fieldA"].asString)
assertEquals("s2", parseJson["fieldB"].asString)
assertEquals("s3", parseJson["fieldC"].asString)

Дальнейшее исследование показывает, что строка, упомянутая здесь и в вопросе, больше похожа на Map на строку.

У меня возникло небольшое недопонимание, когда GSON имеет дело с Map. К этому следует относиться как к дополнительной удобной поддержке, а не к юридической процедуре. Короче говоря, это не должен преобразовываться, и формат данных должен быть фиксированным. Тогда я пойду работать с сервером и преобразованием базы.

Просто оставьте записку здесь. Если кто-то в будущем захочет быстро исправить строку, вы можете взглянуть на ответ @frc129; однако идеальное решение для этого — исправить поставщика данных, чтобы он предоставлял «правильный формат JSON»:

val jsonString = "{\"fieldA\":\"stringData\",\"fieldB\":\"\",\"fieldC\":\"\"}"
val parseJson = Gson().fromJson(jsonString, JsonObject::class.java)
assertEquals(3, parseJson.size())
assertEquals("stringData", parseJson["fieldA"].asString)
assertEquals("", parseJson["fieldB"].asString)
assertEquals("", parseJson["fieldC"].asString)

Другие вопросы по теме