Affects: 3.0.0-M1
When returning object from a controller method using @ResponseBody
, and if the returned type is Page<T>
where T is a class marked as @kotlinx.serialization.Serializable
, kotlinx.serialization is not used, it is unclear what exactly is used, and, assuming it's Jackson that is used, all configuration, all annotations and other properties are ignored.
I'm using custom InstantSerializer that serializes dates as strings for Kotlinx.serialization.
My POJO is annotated as follows
@file:UseSerializers(InstantSerializer::class, UUIDSerializer::class)
@kotlinx.serialization.Serializable
data class GetProductResponse(
/* ... */
@JsonSerialize(using = JacksonInstantSerializer::class)
@JsonFormat(shape = JsonFormat.Shape.STRING)
val createdAt: Instant,
val id: UUID,
)
Returning List"createdAt": "2022-03-29T20:19:30.304149Z"
However, returning Page"createdAt": 1650109008.203314000
. This breaks FE and introduces inconsistency.
I must note that I specifically would like to use strings to serialize dates.
I tried to work around this issue by providing custom Jackson configuration:
- Using code:
@Bean
fun jacksonObjectMapper(): ObjectMapper = ObjectMapper().apply {
registerModule(JavaTimeModule())
disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
- Using application.properties:
spring.mvc.converters.preferred-json-mapper=kotlinx.serialization
spring.jackson.serialization.write_dates_as_timestamps=false
- Using annotations:
@JsonSerialize(using = JacksonInstantSerializer::class)
@JsonFormat(shape = JsonFormat.Shape.STRING)
val createdAt: Instant,
None of the above methods worked, and I'm unable to achieve the desired behavior.
My Controller method is as follows:
@GetMapping("/")
fun getAll(pageable: Pageable, @RequestBody request: GetProductsFilteredRequest?): Page<GetProductResponse> {
return productRepository.findFiltered(
pageable,
/* ... */
).map { it.toResponse() }
}
Condition evaluations report is as follows:
JacksonAutoConfiguration.JacksonObjectMapperConfiguration#jacksonObjectMapper:
Did not match:
- @ConditionalOnMissingBean (types: com.fasterxml.jackson.databind.ObjectMapper; SearchStrategy: all) found beans of type 'com.fasterxml.jackson.databind.ObjectMapper' jacksonObjectMapper (OnBeanCondition)
JacksonHttpMessageConvertersConfiguration.MappingJackson2HttpMessageConverterConfiguration:
Did not match:
- @ConditionalOnProperty (spring.mvc.converters.preferred-json-mapper=jackson) found different value in property 'spring.mvc.converters.preferred-json-mapper' (OnPropertyCondition)
Matched:
- @ConditionalOnClass found required class 'com.fasterxml.jackson.databind.ObjectMapper' (OnClassCondition)
JacksonHttpMessageConvertersConfiguration.MappingJackson2XmlHttpMessageConverterConfiguration:
Did not match:
- @ConditionalOnClass did not find required class 'com.fasterxml.jackson.dataformat.xml.XmlMapper' (OnClassCondition)
UPDATE: Registering kotlin module for ObjectMapper:
@Bean
fun jacksonObjectMapper(): ObjectMapper = ObjectMapper().apply {
registerModule(JavaTimeModule())
registerKotlinModule()
disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
}
Allows to apply a workaround by using Jackson configuration alongside kotlinx.serialization configuration (only for annotation, other properties are still ignored)
The bug is still valid because kotlinx.serialization is not used for Page content.
Comment From: sdeleuze
For some reasons, kotlinx.serialization does not detect Page<T>
as a collection of T
like it does for List<T>
, despite the fact that Page<T>
extends Iterable<T>
. I have asked more details in https://github.com/Kotlin/kotlinx.serialization/issues/2060#issuecomment-1369808972 so let see Kotlin team feedback.
Comment From: Nek-12
To me it seems like a reasonable thing. Your class may implement Iterable but there's no reason it should always be represented as a collection in a json.
Comment From: sdeleuze
Indeed see https://github.com/Kotlin/kotlinx.serialization/issues/2060#issuecomment-1407581601 related comment.
We could refine kotlinx.serialization
support by replacing serializer = SerializersKt.serializerOrNull(type);
by serializer = SerializersKt.serializerOrNull(this.format.getSerializersModule(), type);
but we would still be blocked by the fact that PolymorphicSerializer
has higher priority than ContextualSerializer
, and I am not confortable implementing a custom logic in Spring because this seems a common need (Ktor has the same). I have described my ask in https://github.com/Kotlin/kotlinx.serialization/issues/2060#issuecomment-1408238074.