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

[Bug] A data class that has an inline class-typed property can't be deserialized #413

Closed
izstas opened this issue Feb 1, 2021 · 11 comments
Labels

Comments

@izstas
Copy link

izstas commented Feb 1, 2021

Describe the bug
A data class that has an inline class-typed property can't be deserialized.

To Reproduce
Run:

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue

inline class MyInlineClass(val i: Int)

data class MyDataClass(val i: MyInlineClass)

fun main() {
    val mapper = jacksonObjectMapper()
    val json = mapper.writeValueAsString(MyDataClass(MyInlineClass(1)))
    println(json)
    val deserialized = mapper.readValue<MyDataClass>(json)
    println(deserialized)
}

Expected behavior
The JSON is properly deserialized.

Actual behavior

{"i":1}
Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `MyDataClass` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (String)"{"i":1}"; line: 1, column: 2]

Or, if there are multiple fields in the data class:

{"i":1,"j":1}
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `MyDataClass` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (String)"{"i":1,"j":1}"; line: 1, column: 2]

Versions
Kotlin: 1.4.21
Jackson-module-kotlin: 2.12.1
Jackson-databind: 2.12.1

Additional context
I've tried to dig through this and discovered that adding an inline class-typed property causes some constructor changes - the compiled data class has two constructors:

  // access flags 0x2
  private <init>(I)V

  // access flags 0x1001
  public synthetic <init>(ILkotlin/jvm/internal/DefaultConstructorMarker;)V

The second one is ignored by Jackson because it's synthetic. The first one is correctly discovered, however, that constructor is invisible through Kotlin reflection, so Jackson-module-kotlin fails to discover the parameter names and is not able to use that constructor.

The problem can not be worked around by using @JsonProperty because Kotlin compiler places it onto the parameters of the synthetic constructor, and no annotations appear on the private constructor.

@izstas izstas added the bug label Feb 1, 2021
@dinomite dinomite added the 2.12 label Feb 7, 2021
@ragnese
Copy link

ragnese commented Apr 8, 2021

This is a well-known issue, but I didn't realize that it had to do with the constructors being synthetic! Is there no way for Jackson to find synthetic stuff at runtime?

@dvail
Copy link

dvail commented Apr 20, 2021

FWIW I'm running into what looks like the same issue using a Kotlin value class running the following versions:

Kotlin: 1.5.0-RC
Jackson-module-kotlin: 2.11.3
Jackson-databind: 2.11.3

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue

@JvmInline
value class MyInlineClass(val i: Int)

data class MyDataClass(val i: MyInlineClass)

data class MyDataClassTwoProps(
     // This annotation seems to be required due to the property name getting mangled
    @get:JsonProperty(value = "i")
    val i: MyInlineClass,
    @get:JsonProperty(value = "j")
    val j: MyInlineClass,
)

fun main() {
        val json = objectMapper.writeValueAsString(MyDataClassTwoProps(MyInlineClass(1), 2))
        println(json)
        val deserialized = objectMapper.readValue<MyDataClassTwoProps>(json)
        println(deserialized)

        val json2 = objectMapper.writeValueAsString(MyDataClass(MyInlineClass(1)))
        println(json2)
        val deserialized2 = objectMapper.readValue<MyDataClass>(json2)
        println(deserialized2)
}

Interestingly enough, the data class with two properties does deserialize correctly, regardless of the type of the second property (either a value/inline class or a normal class). Only the data class with a single value class property throws an exception using the above versions.

@ragnese
Copy link

ragnese commented Apr 20, 2021

@dvail
That one should be easy to fix. Single-field class constructors are often problematic, anyway. Try writing:

data class MyDataClass @JsonCreator(mode = JsonCreator.DELEGATING) constructor(val i: MyInlineClass)

and see what happens. The above is from memory, so I'm not sure if the delegating mode constant is uppercase or what.

You may or may not also need that JsonProperty annotation.

@cowtowncoder
Copy link
Member

What @ragnese said, with just one twist: DELEGATING means that the incoming JSON value must match the argument value, and nothing else -- typically used with simple scalar types like int, boolean or String. Conversely, PROPERTIES is one that would expect a JSON Object with one property, with name matching.

So I guess that if you want behavior similar to 2- or 3-argument case, you would add:

@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)

@ragnese
Copy link

ragnese commented Apr 21, 2021

Sorry- yes, @cowtowncoder is right. I was thinking with the mindset of wanting the class to act like a wrapper, which is what I was just doing recently. To have it act like a nested object, you'd use PROPERTIES

@dvail
Copy link

dvail commented Apr 21, 2021

Thanks for the tips! Unfortunately I was unable to get this working with either DELEGATING or PROPERTIES:

@JvmInline
value class MyInlineClass(private val i: Int)

data class MyDataClass @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) constructor(
    @get:JsonProperty(value = "i")
    val i: MyInlineClass,
)

fun main() {
        val json = objectMapper.writeValueAsString(MyDataClass(MyInlineClass(42)))
        println(json)
        val deserialized = objectMapper.readValue<MyDataClass>(json)
        println(deserialized)
}

What did serve as a workaround in my case though was defining a static create method on the data class as described in this comment:
#199 (comment)

@JvmInline
value class MyInlineClass(private val i: Int)

data class MyDataClass(
    @get:JsonProperty(value = "i")
    val test: MyInlineClass,
) {
    companion object {
        @JsonCreator
        @JvmStatic
        fun create(test: Int) = MyDataClass(MyInlineClass(test))
    }
}

The key thing here was that the name of the parameter passed to create must be the same name as the parameter passed to the data class.

@cowtowncoder
Copy link
Member

cowtowncoder commented Apr 21, 2021

One comment on test: please avoid "write then read immediately" construct on reproductions; or, if using both, verify both intermediate JSON and resulting object. This way it is possible to reason about case more easily. This because serialization and deserialization sides are related but not closely coupled, so it is usually necessary to consider them separately.
In this case, in particular, it is important to know actual JSON being read and how that matches to K Object.

Similarly, instead of printing out things, assertions are good as they point out expectations: showing just what happens does not always tell what your expectation was (and verbal descriptions are more ambiguous than assert statements).

@fzoli
Copy link

fzoli commented Sep 20, 2022

Here is a complete test code based on the previous message of @dvail

Tested module:
com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4

import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test

@JvmInline
value class MyInlineClass(val i: Int)

data class MyDataClass(val i: MyInlineClass)

data class MyDataClassTwoProps(
    @get:JsonProperty(value = "i")
    val i: MyInlineClass,
    @get:JsonProperty(value = "j")
    val j: MyInlineClass,
)

data class MyNormalDataClass(val i: Int)

class ObjectMapperInlineClassTest {

    private val objectMapper = jacksonObjectMapper()

    @Test
    fun `data class without inline class`() {
        val obj = MyNormalDataClass(1)

        val json = objectMapper.writeValueAsString(obj)
        Assertions.assertEquals("{\"i\":1}", json)

        val deserialized = objectMapper.readValue<MyNormalDataClass>(json) // works
        Assertions.assertEquals(obj, deserialized)
    }

    @Test
    fun `data class with one inline class`() {
        val obj = MyDataClass(MyInlineClass(1))

        val json = objectMapper.writeValueAsString(obj)
        Assertions.assertEquals("{\"i\":1}", json)

        val deserialized = objectMapper.readValue<MyDataClass>(json) // throws MismatchedInputException exception
        Assertions.assertEquals(obj, deserialized)
    }

    @Test
    fun `data class with two inline class`() {
        val obj = MyDataClassTwoProps(MyInlineClass(1), MyInlineClass(2))

        val json = objectMapper.writeValueAsString(obj)
        Assertions.assertEquals("{\"i\":1,\"j\":2}", json)

        val deserialized = objectMapper.readValue<MyDataClassTwoProps>(json) // throws MismatchedInputException exception
        Assertions.assertEquals(obj, deserialized)
    }

}

@hmchangm
Copy link

hmchangm commented Nov 8, 2022

I had some tests. For data class with 2+ inline value fields is OK with the following module register.
For data class with 1 field can use JsonIgnore field to workaround.
Back to basic, a data class with only 1 value field means the value class self can represent as domain model and no need data class wrapper.

https://github.com/hmchangm/getting-start-QK/blob/master/src/test/kotlin/tw/brandy/ironman/InlineJacksonTest.kt

import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule
import io.quarkus.test.junit.QuarkusTest
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import javax.inject.Inject


@JvmInline
value class MyInlineClass(val i: Int)

data class MyDataClass(val i: MyInlineClass, @JsonIgnore val _i:Int=0)

data class MyDataClassTwoProps(
    val i: MyInlineClass,
    val j: MyInlineClass
)

data class MyNormalDataClass(val i: Int)

class ObjectMapperInlineClassTest {

    val objectMapper : ObjectMapper = jacksonObjectMapper()
        .registerModule(Jdk8Module()).registerModule(ParameterNamesModule())

    @Test
    fun `data class without inline class`() {
        val obj = MyNormalDataClass(1)

        val json = objectMapper.writeValueAsString(obj)
        Assertions.assertEquals("""{"i":1}""", json)

        val deserialized = objectMapper.readValue(json,MyNormalDataClass::class.java) // works
        Assertions.assertEquals(obj, deserialized)
    }

    @Test
    fun `data class with one inline class`() {
        val obj = MyDataClass(MyInlineClass(1))

        val json = objectMapper.writeValueAsString(obj)
        Assertions.assertEquals("""{"i":1}""", json)

        val deserialized = objectMapper.readValue(json,MyDataClass::class.java)
        Assertions.assertEquals(obj, deserialized)
    }

    @Test
    fun `data class with two inline class`() {
        val obj = MyDataClassTwoProps(MyInlineClass(1), MyInlineClass(2))

        val json = objectMapper.writeValueAsString(obj)
        Assertions.assertEquals("""{"i":1,"j":2}""", json)

        val deserialized = objectMapper.readValue(json,MyDataClassTwoProps::class.java) // throws MismatchedInputException exception
        Assertions.assertEquals(obj, deserialized)
    }

}

@k163377
Copy link
Contributor

k163377 commented Feb 13, 2023

I am working on deserialization support for value class in an experimental project I created (jackson-module-kogera), so please give it a try.
I would appreciate a star to keep me motivated.

I will basically report on the progress of this project in #199.

@k163377
Copy link
Contributor

k163377 commented Mar 17, 2023

This issue is closed as the issue regarding deserialization support for value class related content will be summarized in #650.

@k163377 k163377 closed this as completed Mar 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

8 participants