Android: парсим JSON правильно

Опасность = JSON + значения по умолчанию
3 минуты1100

Любой Android-разработчик рано или поздно сталкивается с форматом представления данных типа JSON. Наиболее часто он используется для передачи/получения данных с какого-либо сервера. Формат предельно прост, подробнее о нём можно почитать в Википедии:

«JSON (JavaScript Object Notation, обычно произносится как /ˈdʒeɪsən/ JAY-sən) — текстовый формат обмена данными, основанный на JavaScript. Как и многие другие текстовые форматы, JSON легко читается людьми. Несмотря на происхождение от JavaScript, формат считается независимым от языка и может использоваться практически с любым языком программирования. Для многих языков существует готовый код для создания и обработки данных в формате JSON».

Любой класс в Java или Kotlin можно представить в виде структуры JSON, где есть поля, атрибуты, фигурные скобки обозначают объект, квадратные скобки — массив.

Следующий пример показывает JSON-представление данных об объекте, описывающем человека. В данных присутствуют строковые поля имени и фамилии, информация об адресе и массив, содержащий список телефонов. Как видно из примера, значение может представлять собой вложенную структуру:

{
   "firstName": "Иван",
   "lastName": "Иванов",
   "address": {
       "streetAddress": "Московское ш., 101, кв.101",
       "city": "Ленинград",
       "postalCode": 101101
   },
   "phoneNumbers": [
       "812 123-1234",
       "916 123-4567"
   ]
}
 

Наиболее популярный среди разработчиков способ трансформировать данные в JSON и обратно — это библиотека GSON от самих разработчиков Google. Она очень проста в использовании, мало весит и интегрирована во многие библиотеки. 

На данный момент все, за редким исключением, Android-разработчики используют Kotlin, а GSON (как и другие подобные библиотеки типа Jackson или Moshi) написана на Java. Это не страшно, потому что Kotlin и Java полностью взаимозаменяемы, но есть небольшие нюансы, которые могут привести к совершенно неожиданным результатам.

Давайте создадим класс User и посмотрим на эти нюансы на практике. В этом классе у нас будут обычные поля и поля со значениями по умолчанию. Как вы знаете, в Java нельзя присваивать переменным значения по умолчанию, а в Kotlin можно:

data class User(
    val name: String,
    val email: String,
    val age: Int = 13,
    val role: Role = Role.Viewer
)
 
enum class Role { Viewer, Editor, Owner }

И теперь представим, что с какого-то сервера пришли данные о пользователе в формате JSON:

{
   "name" : "John Doe",
   "email" : "john.doe@email.com"
}

Теперь нам нужно распарсить этот JSON и превратить его в обычный класс Kotlin с помощью библиотеки GSON. Добавим зависимость GSON в наш проект в файл Gradle

implementation 'com.google.code.gson:gson:2.8.6'

и сразу напишем тест:

class JsonUnitTest {
 
    private val jsonString = """
            {
                "name" : "John Doe",
                "email" : "john.doe@email.com"
            }
        """
 
    @Test
    fun gsonTest() {
        val user = Gson().fromJson(jsonString, User::class.java)
 
        assertEquals("John Doe",user.name)
        assertEquals(null, user.role)
        assertEquals(0, user.age)
        //User(name=John Doe, email=john.doe@email.com, age=0, role=null)
    }
}

Тест прекрасно выполняется без единой ошибки, то есть код работает. Но обратите внимание, что возраст пользователя у нас == 0, а его роль не определена, хотя в самом классе у нас прописаны значения по умолчанию для этих переменных. Если эти параметры не определены в JSON, должны подставляться значения по умолчанию: возраст == 13, а роль == Viewer, но они не подставляются, а код всё равно работает. Вот так неожиданность! Не такого поведения мы ожидали!

Давайте разбираться. Дело в том, что, как мы писали выше, библиотека GSON написана на Java, а это значит, что значения по умолчанию для несуществующих полей такие: для примитива int — это 0, для отсутствующего объекта — это null. Простая трансформация JSON в класс на Kotlin может легко сломать null-safety, на который так рассчитывают все разработчики, и может привести к падению приложения там, где оно падать не должно. 

И тут нам на помощь приходит котлиновская библиотека по сериализации объектов.

Сериализация от Kotlin

Это небольшая вспомогательная библиотечка от разработчиков языка, которая работает с помощью аннотации @Serializable. С ней у вас не будет проблем при использовании полей по умолчанию. Чтобы подключить библиотеку к своему проекту, нужно прописать в файле Gradle плагин и несколько зависимостей. В файле проекта build.gradle(Project):

buildscript {
   repositories {
       google()
       mavenCentral()
   }
   dependencies {
       classpath "com.android.tools.build:gradle:7.0.2"
       classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31"
       classpath "org.jetbrains.kotlin:kotlin-serialization:1.5.31"
   }
}

В файле проекта build.gradle(Module:app):

plugins {
   id 'com.android.application'
   id 'kotlin-android'
   id 'kotlinx-serialization'
}
 
dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0"
}

Теперь трансформация JSON (сериализация) будет проходить корректно. Напишем наш класс и добавим аннотацию:

@Serializable
data class User(
    val name: String,
    val email: String,
    val age: Int = 13,
    val role: Role = Role.Viewer
)
 
enum class Role { Viewer, Editor, Owner }

Протестируем:

class JsonUnitTest {
 
    private val jsonString = """
            {
                "name" : "John Doe",
                "email" : "john.doe@email.com"
            }
        """.trimIndent()
 
    @Test
    fun gsonTest() {
        val user = Gson().fromJson(jsonString, User::class.java)
 
        assertEquals("John Doe", user.name)
        assertEquals(null, user.role)
        assertEquals(0, user.age)
        //User(name=John Doe, email=john.doe@email.com, age=0, role=null)
    }
 
    @Test
    fun jsonTest() {
        val user = Json.parse(User.serializer(), jsonString)
 
        assertEquals("John Doe", user.name)
        assertEquals(Role.Viewer, user.role)
        assertEquals(13, user.age)
       //User(name=John Doe, email=john.doe@email.com, age=13, role=Viewer)
    }
}

Тест пройден успешно! Теперь класс сериализуется у нас со значениями по умолчанию, если таковые прописаны в классе.

Сериализация от Kotlin + Retrofit

Если вы хоть раз отправляли запрос на сервер или получали с сервера какой-то ответ, то наверняка вы знакомы с библиотекой Retrofit. В этой библиотеке нет поддержки сериализации от Kotlin, но у вас есть возможность добавить вспомогательную библиотеку от Джека Вортона в качестве зависимости Gradle:

dependencies {
//Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
}

Теперь при использовании Retrofit сериализация будет происходить автоматически:

val contentType = "application/json".toMediaType()
val retrofit = Retrofit.Builder()
.baseUrl("https://www.example.com")
.addConverterFactory(Json(JsonConfiguration(strictMode = false)).asConverterFactory(contentType))
.build()

Дополнительно, но не обязательно вы можете использовать JsonConfiguration для выключения StrictMode. StrictMode включен по умолчанию и запрещает использование неизвестных ключей в JSON и нечисловые значения в числах с плавающей точкой. Хорошая практика — включать StrictMode в «дебажной» версии приложения и выключать его в «релизной».

Читайте больше полезных статей для начинающих Android-разработчиков:

А если затянет — приходите на факультет Android-разработки. В время учебы вы разработаете Android-приложение и выложите его в Google Play, даже если никогда не программировали. А также своите языки Java и Kotlin, командную разработку, Material Design и принципы тестирования.

программированиеandroidгайдтуториал
Нашли ошибку в тексте? Напишите нам.
Спасибо,
что читаете наш блог!