Skip to content

Discussion: Non-intrusive serialization declaration - Reducing annotation pollution in KMP/JVM ecosystemsΒ #3086

@zjarlin

Description

@zjarlin

Problem Statement: The Annotation Intrusiveness Dilemma
Currently, Kotlinx Serialization follows an intrusive approach where classes must be annotated with @serializable to enable serialization. This creates several ecosystem challenges:

The Real-world Pain Point
kotlin
// Library author's code - they don't care about serialization
data class ApiResponse(
val data: String,
val status: Int,
val metadata: Map<String, Any>
)

// Consumer wants to serialize this - BUT CAN'T!
// They're forced to use workarounds:
val response = ApiResponse("hello", 200, emptyMap())

// ❌ This doesn't work - no @serializable annotation
// Json.encodeToString(response)

// βœ… Consumers must create wrapper classes - boilerplate heaven!
@serializable
data class SerializableApiResponse(
val data: String,
val status: Int,
val metadata: Map<String, Any>
) {
constructor(original: ApiResponse) : this(...)
}
Ecosystem Impact
Library Authors: Forced to add Kotlinx Serialization as a dependency and annotate their models

KMP Compatibility: Libraries aiming for minimal dependencies can't use serialization without forcing it on consumers

Legacy Code: Impossible to serialize classes from Java libraries or older Kotlin code

Annotation Pollution: Every data class gets @serializable whether it needs serialization or not

Proposed Solution: Consumer-side Declaration
Enable serialization declaration at the consumption site rather than the declaration site.

Option A: File-level Declaration
kotlin
// In consumer module - no library modification needed
@file:GenerateSerializersFor(
com.some.library.ApiResponse::class,
com.some.library.UserDTO::class
)

fun main() {
val response = ApiResponse("data", 200, mapOf())
val json = Json.encodeToString(response) // Just works! πŸŽ‰
}
Option B: Configuration-based
kotlin
// serialization-config.kts
externalClasses {
serializable(ApiResponse::class)
serializable(UserDTO::class) {
rename("userId" to "user_id")
}
}
Option C: Extension-based
kotlin
// Declare serializability via extensions
val ApiResponse.serializer: KSerializer
@ExperimentalSerializationApi
get() = generatedSerializer()
Benefits for KMP/JVM Ecosystem
For Library Authors
Zero dependency: No need to depend on kotlinx-serialization

Clean API: Models remain framework-agnostic

Better KMP support: Minimal dependencies for multiplatform libraries

For Consumers
No wrappers: Direct serialization of any class

Flexibility: Choose serialization strategy per use case

Migration friendly: Gradually adopt serialization without refactoring

For Kotlin Ecosystem
Interop: Serialize Java classes, third-party library models

Progressive: Works alongside existing @serializable approach

Tooling: IDE support for external class configuration

Technical Considerations
Compiler Plugin Extension: Extend KCP to process consumer declarations

Symbol Resolution: Handle external classes from binaries/dependencies

Module System: Ensure generated serializers are properly registered

Incremental Compilation: Maintain build performance

Community Impact
This change would position Kotlinx Serialization as a more ecosystem-friendly solution, competing better with reflection-based alternatives like Jackson while maintaining compile-time safety.

Discussion Points
Is consumer-side declaration feasible with current compiler architecture?

What's the optimal syntax for declaring external serializers?

How to handle complex cases (custom serializers, polymorphic hierarchies)?

Should this be a separate plugin or integrated into the main one?

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions