kotlinx.serialization icon indicating copy to clipboard operation
kotlinx.serialization copied to clipboard

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

Open zjarlin opened this issue 2 months ago • 5 comments

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<ApiResponse> @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?

zjarlin avatar Sep 30 '25 14:09 zjarlin

I'm sorry I didn't notice that this feature was already in the documentation ;Deriving external serializer for another Kotlin class (experimental) by the way, ask if there is a way to scan the package, so it will be lazier.

zjarlin avatar Sep 30 '25 15:09 zjarlin

I'm sorry I didn't notice that this feature was already in the documentation ;Deriving external serializer for another Kotlin class (experimental) by the way, ask if there is a way to scan the package, so it will be lazier.

As to declarative writing of serializers, it ends up being very similar to directly writing a custom serializer, but with more abstraction overheads. Providing serializer access through an extension function is mostly equivalent to having a file level annotation to have it used/picked up. Note that serializing your ApiResponse does not require a separate class, it just requires a custom serializer.

pdvrieze avatar Oct 01 '25 09:10 pdvrieze

Image I need examples that are both custom serialized and consumer-defined, with generic parameters

zjarlin avatar Oct 01 '25 12:10 zjarlin

As the error message says, your FormFieldSerializerConsumer would need to be a class (the same as the FormFieldSerializer) – in other words they would need to be identical in some way. You should however note that it is not possible to have an externally generated serializer for FormField as it is a sealed class (it would be "generated" as an instance of SealedSerializer anyway.

pdvrieze avatar Oct 01 '25 15:10 pdvrieze

For the problem of having a library contain serializers, but those not being used by a consumer, this is where minification would come in. Looking at the way the plugin generates things, serialization is present in a number of functions/constructors and fields of the class and its companion (it appears that using the class without the serialization library will break even if serialization is not used).

pdvrieze avatar Oct 01 '25 18:10 pdvrieze