google / gson

A Java serialization/deserialization library to convert Java Objects into JSON and back
Apache License 2.0
23.23k stars 4.27k forks source link

Gson behaviour is different in Unit test and in actual build #2713

Open The-Pascal opened 1 month ago

The-Pascal commented 1 month ago

Gson version

2.11.0

Java / Android version

JDK 17 Sample of my build.gradle

android {
    namespace = "com.test.package"
    compileSdk = Config.compileSdk

    defaultConfig {
        minSdk = Config.minSdk
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    compileOptions {
        sourceCompatibility = Config.sourceCompatibility
        targetCompatibility = Config.targetCompatibility
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

dependencies {
    testImplementation("junit:junit:4.13.2")
    testImplementation("androidx.test:core:1.6.1")

    implementation("com.google.code.gson:gson:2.11.0")
}

Description

While using Gson in unit tests, when converting json string to data class, if json string doesn't contains some fields, then it falls to default of data type like for nullables it falls to null. However, on actual builds, it's taking default values when json string doesn't contains some fields.

Expected behavior

Both actual app behaviour and unit tests behaviour for Gson should be same.

Actual behavior

In actual app if fields are missing, then it takes data class default values. In unit tests, if fields are missing, then it takes data type default (like for int - 0, for nullables - null, etc)

Reproduction steps

Run this unit test using android studio.


class JsonConverterTest {

    data class Person(
        val name: String,
        val age: Int,
        val isVerified: Boolean = false,
        val margin: Float = 1.0f,
        val gains: Double = 2.0,
        val profit: Double? = 4.0,
        val friends: List<String>? = listOf()
    )

    @Test
    fun `test gson serialization`() {
        val jsonString = """{"name":"John","age":30,"isVerified":true,"margin":1.0,"gains":5.0}"""
        val actual = Gson().fromJson(jsonString, Person::class.java)

        val expected = Person("John", 30, true, 1.0f, 5.0, 4.0, listOf())

        assertWithMessage("Failed for case: $jsonString").that(actual).isEqualTo(expected)
    }
}

Output after running above unit test

Failed for case: {"name":"John","age":30,"isVerified":true,"margin":1.0,"gains":5.0}
expected: Person(name=John, age=30, isVerified=true, margin=1.0, gains=5.0, profit=4.0, friends=[])
but was : Person(name=John, age=30, isVerified=true, margin=1.0, gains=5.0, profit=null, friends=null)

Expected output

Test should get passed.

PS: let me know if I missed something

Marcono1234 commented 1 month ago

The behavior of the unit test is likely caused by Gson's limited support for Kotlin (see also the Gson README and #2666). Gson does not consider default values for constructor and function arguments, and because your class does not have a no-args constructor, Gson will fall back to using Unsafe to create the instance, which does not invoke any constructor. If you use GsonBuilder.disableJdkUnsafe(), it will probably fail, indicating that it previously relied on Unsafe.

Not sure why it works as expected in release mode. Maybe the code shrinker R8 is rewriting the code in a way which causes the class to have a no-args constructor. Out of curiosity, if you disable R8, does the release build then behave in the same (unexpected) way as the unit test?

We already have other GitHub issues for not well supported Kotlin features, see the https://github.com/google/gson/labels/kotlin label and particularly #1657. So this issue here is probably a duplicate.

The-Pascal commented 1 month ago

Thanks for quick reply @Marcono1234 I will check with disabling R8, if the release build behaves the same then