Can't understand kotlin's manner that "privatize" members of a `data class`

Greetings community!

Let’s say in my Android project I have a data class:

data class Foo(
    val a: Int,
    val b: String,
)

Now if I use kotlin reflection to access its fields, all goes well. The console will print:

a PUBLIC Success(1)
b PUBLIC Success(b)

However, this only works for debug build. If building the app in release mode, things will change, and I found that, all members became PRIVATE, which caused a reflection access failure.

a PRIVATE Failure(kotlin.reflect.full.IllegalCallableAccessException: java.lang.IllegalAccessException: Class java.lang.Class<kotlin.reflect.jvm.internal.calls.CallerImpl$FieldGetter> cannot access private final  field int org.example.kotlinreflection.Foo.a of class java.lang.Class<org.example.kotlinreflection.Foo>)
b PRIVATE Failure(kotlin.reflect.full.IllegalCallableAccessException: java.lang.IllegalAccessException: Class java.lang.Class<kotlin.reflect.jvm.internal.calls.CallerImpl$FieldGetter> cannot access private final  field java.lang.String org.example.kotlinreflection.Foo.b of class java.lang.Class<org.example.kotlinreflection.Foo>)

In release build, Foo’s public fields become Kotlin’s private properties, and getters and setters are generated correspondingly. Directly accessing the private members is forbidden.

This makes me confused. If we want the fields be private, we can just add the private modifier; fields declared as public (though in Kotlin public can be omitted) becoming private implicitly after release build, kinda unintuitive and comfusing for me. Just say, Kotlin-specific reflection methods (methods under K*) can’t access member values in a plain Kotlin data class?

Note class (instead of data class) has the same thing (members becoming private after release build). Btw the annotation @JvmField works, and that’s just because it tells the compiler treat them as vanilla Java members - don’t touch them!

I’m fairly new to Kotlin. Maybe I missed something about Kotlin fields vs properties vs reflection?

Any idea? Thanks.

You are confusing properties and fields. In Kotlin we define properties, and they are more about getters/setters than fields. Fields are just a way to implement properties and they are hidden by default as an implementation details.

It is a common practice in Java as well to (almost) never define public fields, and instead create private fields + public getters/setters. Kotlin just made this easier to do.

See: Property (programming) - Wikipedia

2 Likes

Thanks for the reply. yea I basically understand properties vs fields. But now, in this case, how can one define a pure data class with fields (but not the private intermediate properties) that will make deserialization libraries happy (like Gson etc)? Attach @JvmField on every members?

FYI what i’m doing is deserializing an object to a Ktor FormDataContent. The code:

inline fun <reified T : Any> fromObject(obj: T): FormDataContent {
        return FormDataContent(Parameters.build {
            // only applicable for members with @JvmField
            T::class.memberProperties.forEach {
                val name = it.name
                val value = it.getter.call(obj)
                value?.let { v ->
                    append(name, v.toString())
                }
            }
        })
    }

Yes, I believe @JvmField is the way to go.

Longer answer and my personal opinion is that serialization frameworks, dependency injection, etc., should optimally use getters/setters. Or support both props and fields and let users choose. AFAIK, from the very beginning, users ask GSON authors to include support for props and they ignore this. Anyway, I would expect specifically GSON to work correctly, because it can access private fields.

2 Likes

In data classes all main properties defined in primary constructor already have fields. If you want to filter all the other non-field backed properties you can write this:

T::class.memberProperties
	.filter { it.javaField != null }
	.forEach {
		it(obj)?.let { value ->
			append("${it.name} = $value")
		}
	}