jackson-module-kotlin icon indicating copy to clipboard operation
jackson-module-kotlin copied to clipboard

Kotlin sealed class property is always last despite SORT_PROPERTIES_ALPHABETICALLY

Open antonkushchn26 opened this issue 5 years ago • 6 comments

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")
}

antonkushchn26 avatar May 26 '20 12:05 antonkushchn26

Version: 2.10.3

antonkushchn26 avatar May 26 '20 14:05 antonkushchn26

Kotlin-specific (unless proven not to be, with java-only reproduction), transferred.

cowtowncoder avatar Sep 29 '20 23:09 cowtowncoder

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 avatar Oct 16 '20 19:10 hiddewie

@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 avatar Oct 16 '20 23:10 cowtowncoder

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

hiddewie avatar Oct 17 '20 12:10 hiddewie

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.

remen avatar Apr 18 '22 08:04 remen

The issue has not been resolved, but is closed as it is a duplicate of #271.

k163377 avatar Feb 22 '23 15:02 k163377