Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvement to allow customization of kotlin serializer in AbstractKotlinSerializationHttpMessageConverter #29761

Conversation

meloning
Copy link
Contributor

@meloning meloning commented Jan 3, 2023

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]

        @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 the hasPolymorphism() function.
      스크린샷, 2023-01-03 20-14-16
      image
      image

[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?
)

스크린샷, 2023-01-03 20-47-12

[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.

…r.serializer method

* Add serializerInternal abstract method to extend serializer creation
* Implement serializerInternal abstract method in KotlinSerializationBinaryHttpMessageConverter, KotlinSerializationJsonHttpMessageConverter
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jan 3, 2023
@meloning
Copy link
Contributor Author

meloning commented Jan 3, 2023

[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?

@sdeleuze sdeleuze self-assigned this Jan 3, 2023
@sdeleuze sdeleuze added the theme: kotlin An issue related to Kotlin support label Jan 3, 2023
@sdeleuze
Copy link
Contributor

sdeleuze commented Jan 3, 2023

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 #28389 (comment) and the related question discussed on Kotlin/kotlinx.serialization#2060 (comment) where you should find the answer to your question "Why not allow Open Polymorphic Serialization?".

@sdeleuze sdeleuze closed this Jan 3, 2023
@meloning
Copy link
Contributor Author

meloning commented Jan 3, 2023

Ok, I hope it will be resolved soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: superseded An issue that has been superseded by another theme: kotlin An issue related to Kotlin support
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants