I found that Page Generic Type does not work with kotlin serialization.
Reference Issue: Issue-28389
As a result of debugging and analysis, I found the following problems.
[Problem]
* For page types, AbstractKotlinSerializationHttpMessageConverter.serialize()
always returns null, so AbstractKotlinSerializationHttpMessageConverter.canWrite()
will return false. Therefore, KotlinSerialization is ignored by AbstractMessageConverterMethodProcessor.writeWithMessageConverters()
.
@Override
public boolean canWrite(@Nullable Type type, Class<?> clazz, @Nullable MediaType mediaType) {
if (serializer(type != null ? GenericTypeResolver.resolveType(type, clazz) : clazz) != null) {
return canWrite(mediaType);
}
else {
return false;
}
}
/**
* Tries to find a serializer that can marshall or unmarshall instances of the given type
* using kotlinx.serialization. If no serializer can be found, {@code null} is returned.
* <p>Resolved serializers are cached and cached results are returned on successive calls.
* @param type the type to find a serializer for
* @return a resolved serializer for the given type, or {@code null}
*/
@Nullable
private KSerializer<Object> serializer(Type type) {
KSerializer<Object> serializer = serializerCache.get(type);
if (serializer == null) {
try {
serializer = SerializersKt.serializerOrNull(type);
}
catch (IllegalArgumentException ignored) {
}
if (serializer != null) {
if (hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) {
return null;
}
serializerCache.put(type, serializer);
}
}
return serializer;
}
- Why does
AbstractKotlinSerializationHttpMessageConverter.serialize()
always return null? - It always returns a PolymorphicSerializer instance because Page is an interface type in
Serializer sKt.serializeOrNull()
. - However, since the registered
KotlinSerializationJsonHttpMessageConverter
does not support open Polymorphic Serialization, null is returned due to thehasPolymorphism()
function.
[Temporarily Resolved]
I understood the above problems and solved them by customizing AbstractKotlinSerializationHttpMessageConverter
and registering it.
- Create PageSerializer
class PageSerializer<T>(
private val dataSerializer: KSerializer<List<T>>
) : KSerializer<Page<T>> {
override val descriptor: SerialDescriptor = dataSerializer.descriptor
override fun serialize(encoder: Encoder, value: Page<T>) = dataSerializer.serialize(encoder, value.content)
override fun deserialize(decoder: Decoder) = PageImpl(dataSerializer.deserialize(decoder))
}
- Customize
AbstractKotlinSerializationHttpMessageConverter.serialize()
->CustomKotlinSerializationJsonHttpMessageConverter
class
@Suppress("UNCHECKED_CAST")
@OptIn(ExperimentalSerializationApi::class)
override fun serializerInternal(type: Type): KSerializer<Any>? {
return when(type) {
is ParameterizedType -> {
val rootClass = (type.rawType as Class<*>)
val args = (type.actualTypeArguments)
val argsSerializers = args.map { serializerOrNull(it) }
if (argsSerializers.isEmpty() && argsSerializers.first() == null) return null
when {
Page::class.java.isAssignableFrom(rootClass) ->
PageSerializer(ListSerializer(argsSerializers.first()!!.nullable)) as KSerializer<Any>?
else -> null
}
}
else -> serializerOrNull(type)
}
}
- Register CustomKotlinSerializationJsonHttpMessageConverter
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>>) {
converters.addAll(listOf(CustomKotlinSerializationJsonHttpMessageConverter(), MappingJackson2HttpMessageConverter()))
println(converters)
}
}
As a result, it was successful.
@Serializable
data class UserDto(
val id: Long,
val name: String,
val phone: String,
@SerialName("isActive")
@JsonProperty("isActiveJackson")
val active: Boolean,
val createdAt: String?,
val updatedAt: String?
)
[My opinion] I think it is a very inconvenient process for users to create and register CustomKotlinSerializationHttpMessageConverter as above every time. And since Page, Slice, etc. are the types that users use a lot, the above process will be more inconvenient.
I tried to solve it by forking the Spring-Data-Commons module that owns the Page class, but found out that the access specifier of AbstractKotlinSerializationHttpMessageConverter.serialize()
was private, so I considered making serializeInternal abstract method.
If PR is allowed, I think I can expect the following two effects. 1. Expectations that can be extended by adding KotlinSerializationHttpMessageConverter for Page, Slice in Spring-Data-Commons Module 2. Expectations that users can expand by creating and registering the necessary custom serializers in the project as needed
[Plan] * I will add KotlinSerializeHttpMessageConverter for Page and Slice in Spring-Data-Commons module like the contents of Comment.
Comment From: meloning
[Question] 1. Why not allow Open Polymorphic Serialization? Reference Link 2. Do I need test code for serializeInternal abstract method? I'm asking because it's no different than testing SerializersKt. 3. If PR is allowed, would it be good to modify the kotlin serialization in the spring-webflux module as well?
Comment From: sdeleuze
Thanks for your detailed analysis and PR, I think I would like to avoid such advanced configuration and find a more straightforward solution. Let's try to move forward on this via https://github.com/spring-projects/spring-framework/issues/28389#issuecomment-1369811407 and the related question discussed on https://github.com/Kotlin/kotlinx.serialization/issues/2060#issuecomment-1369808972 where you should find the answer to your question "Why not allow Open Polymorphic Serialization?".
Comment From: meloning
Ok, I hope it will be resolved soon.