Kotlin sealed class property is always last despite SORT_PROPERTIES_ALPHABETICALLY
Looks like the property from sealed class is always last despite SORT_PROPERTIES_ALPHABETICALLY is set to true.
val om = jacksonObjectMapper().apply { configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true)
}
fun main() {
om.writeValueAsString(Test(2, 1)) // {"a":1,"b":2}
val user = User.Customer(UUID.fromString("7c183904-63f7-40d3-936b-7f4ae7f96189"))
om.writeValueAsString(user) // {"userId":"7c183904-63f7-40d3-936b-7f4ae7f96189","type":"customer"}
}
data class Test(
val b: Int,
val a: Int
)
sealed class User(val type: String) {
data class Customer(val userId: UUID) : User("customer")
data class Employee(val email: String) : User("employee")
}
Version: 2.10.3
Kotlin-specific (unless proven not to be, with java-only reproduction), transferred.
The following test case also fails, which uses no sealed classes, only 'normal' inheritance.
open class Parent(val a: String)
class Child(val b: String) : Parent(b)
@Test
fun inheritingClassesShouldHaveSortedOutput() {
val objectMapper = jacksonMapperBuilder()
.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)
.build()
val child = Child("abc")
// The output should be sorted
assertEquals("{\"a\":\"abc\",\"b\":\"abc\"}", objectMapper.writeValueAsString(child))
}
Output:
java.lang.AssertionError: Expected <{"a":"abc","b":"abc"}>, actual <{"b":"abc","a":"abc"}>.
at kotlin.test.DefaultAsserter.fail(DefaultAsserter.kt:16)
...
This does not fail when removing the KotlinModule from the ObjectMapper.
@hiddewie Would it be easy as a next step to change classes to be plain Java? If so, I could then produce on databind side.
One relevant thing here is that by default @JsonCreator associated properties are ordered before other properties, although there is a PR to allow change this:
https://github.com/FasterXML/jackson-databind/pull/2879
which may fundamentally be what happens here.
@cowtowncoder
I will attach the entire test class (4 test cases)
package com.fasterxml.jackson.module.kotlin.test.github
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder
import org.junit.Test
import java.util.*
import kotlin.test.assertEquals
class Github377 {
sealed class User(val type: String) {
data class Customer(val userId: UUID) : User("customer")
data class Employee(val email: String) : User("employee")
}
@JsonSubTypes(
JsonSubTypes.Type(name = "customer2", value = JavaCustomer::class),
JsonSubTypes.Type(name = "employee2", value = JavaEmployee::class)
)
open class JavaUser(val type: String)
data class JavaCustomer(val userId: UUID) : JavaUser("customer")
data class JavaEmployee(val email: String) : JavaUser("employee")
open class Parent(val a: String)
class Child(val b: String) : Parent(b)
@Test
fun inheritingClassesShouldHaveSortedOutput() {
val objectMapper = jacksonMapperBuilder()
.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)
.build()
val child = Child("abc")
// The output should be sorted
assertEquals("{\"a\":\"abc\",\"b\":\"abc\"}", objectMapper.writeValueAsString(child))
}
@Test
fun testOrderOfKotlinSealedClassProperty() {
val objectMapper = jacksonMapperBuilder()
.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)
.build()
val user = User.Customer(UUID.fromString("7c183904-63f7-40d3-936b-7f4ae7f96189"))
// The output should be sorted
assertEquals("{\"type\":\"customer\",\"userId\":\"7c183904-63f7-40d3-936b-7f4ae7f96189\"}", objectMapper.writeValueAsString(user))
}
@Test
fun testOrderOfInheritedClassPropertyWithKotlinModule() {
val objectMapper = jacksonMapperBuilder()
.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)
.build()
val user = JavaCustomer(UUID.fromString("7c183904-63f7-40d3-936b-7f4ae7f96189"))
// The output should be sorted
assertEquals("{\"type\":\"customer\",\"userId\":\"7c183904-63f7-40d3-936b-7f4ae7f96189\"}", objectMapper.writeValueAsString(user))
}
@Test
fun testOrderOfInheritedClassPropertyWithoutKotlinModule() {
val objectMapper = JsonMapper.builder()
.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)
.build()
val user = JavaCustomer(UUID.fromString("7c183904-63f7-40d3-936b-7f4ae7f96189"))
// The output should be sorted
assertEquals("{\"type\":\"customer\",\"userId\":\"7c183904-63f7-40d3-936b-7f4ae7f96189\"}", objectMapper.writeValueAsString(user))
}
}
Of the 4 tests, testOrderOfInheritedClassPropertyWithoutKotlinModule pasess. The other three fail. The common theme is that those using the KotlinModule fail, and those without pass.
So my conclusion is that any inheritance situation with sorted alphabetically keys is broken when using the Jackson Kotlin module.
Quick update on this. It does indeed look like the kotlin module identifies constructors as "creators" which changes the order unless overridden.
I used the tests written by @hiddewie above, but added .disable(MapperFeature.SORT_CREATOR_PROPERTIES_FIRST) as suggested by @cowtowncoder . Then all the tests passed.
The issue has not been resolved, but is closed as it is a duplicate of #271.