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

CSV deserialization fails for targets with value class field when column reordering is enabled #800

Open
3 tasks done
marvk opened this issue May 28, 2024 · 4 comments
Open
3 tasks done
Labels

Comments

@marvk
Copy link

marvk commented May 28, 2024

Search before asking

  • I searched in the issues and found nothing similar.
  • I searched in the issues of databind and other modules used and found nothing similar.
  • I have confirmed that the problem only occurs when using Kotlin.

Describe the bug

When trying to deserialize a CSV into a target class that contains a field that is a value class and
the ObjectMapper has a CsvSchema that has column reordering enabled, Jackson will produce the following exception, seemingly ignoring the @JsonProperty annotations:

Exception in thread "main" com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "BAR_HEADER" (class net.marvk.FooWithValueClass), not marked as ignorable (2 known properties: "bar", "baz"])
at [Source: (StringReader); line: 2, column: 41] (through reference chain: net.marvk.FooWithValueClass["BAR_HEADER"])
at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:61)
at com.fasterxml.jackson.databind.DeserializationContext.handleUnknownProperty(DeserializationContext.java:1153)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:2241)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperty(BeanDeserializerBase.java:1793)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperties(BeanDeserializerBase.java:1743)
at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:546)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1493)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:348)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185)
at com.fasterxml.jackson.databind.MappingIterator.nextValue(MappingIterator.java:283)
at com.fasterxml.jackson.databind.MappingIterator.readAll(MappingIterator.java:323)
at com.fasterxml.jackson.databind.MappingIterator.readAll(MappingIterator.java:309)
at net.marvk.MainKt.main(Main.kt:20)
at net.marvk.MainKt.main(Main.kt)

This issue does not exist for data classes or if column reordering is disabled.

data class value class
Column Reordering false
Column Reordering true

Related to #768.

To Reproduce

Please see the cloneable, runnable MCVE here.

The MCVE tests demonstrate that data classes work whether column reordering is enabled or not, and value classes work with column reordering disabled.

Main.kt

import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.ObjectReader
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.dataformat.csv.CsvMapper
import com.fasterxml.jackson.dataformat.csv.CsvParser
import com.fasterxml.jackson.dataformat.csv.CsvSchema
import com.fasterxml.jackson.module.kotlin.kotlinModule

/**
 * Just show me the bug!
 */
fun main() {
    createReader<FooWithValueClass>(true)
        .readValues<FooWithValueClass>(CSV)
        .readAll()
}

val CSV = """
    BAR_HEADER,BAZ_HEADER
    bar1,baz1
""".trimIndent()

val MAPPER =
    CsvMapper
        .builder()
        .addModule(
            SimpleModule()
                .addDeserializer(BazValueClass::class.java, object : JsonDeserializer<BazValueClass>() {
                    override fun deserialize(p: JsonParser, ctxt: DeserializationContext) = BazValueClass(p.valueAsString)
                })
                .addDeserializer(BazDataClass::class.java, object : JsonDeserializer<BazDataClass>() {
                    override fun deserialize(p: JsonParser, ctxt: DeserializationContext) = BazDataClass(p.valueAsString)
                })
        )
        .addModule(kotlinModule())
        .build()

inline fun <reified T> createSchema(columnReordering: Boolean): CsvSchema =
    MAPPER
        .schemaFor(T::class.java)
        .withColumnReordering(columnReordering)
        .withHeader()

inline fun <reified T> createReader(columnReordering: Boolean): ObjectReader =
    MAPPER
        .readerFor(T::class.java)
        .with(CsvParser.Feature.WRAP_AS_ARRAY)
        .with(createSchema<T>(columnReordering))

data class FooWithValueClass(
    @JsonProperty("BAR_HEADER")
    val bar: String?,
    @JsonProperty("BAZ_HEADER")
    val baz: BazValueClass?,
)

data class FooWithDataClass(
    @JsonProperty("BAR_HEADER")
    val bar: String?,
    @JsonProperty("BAZ_HEADER")
    val baz: BazDataClass?,
)

@JvmInline
value class BazValueClass(val value: String)

data class BazDataClass(val value: String)

Test.kt

import io.kotest.assertions.throwables.shouldNotThrowAny
import io.kotest.matchers.collections.shouldContainExactly
import org.junit.jupiter.api.Test

class Test {
    @Test
    fun valueClassWithoutColumnReordering() {
        val actual = shouldNotThrowAny {
            createReader<FooWithValueClass>(false)
                .readValues<FooWithValueClass>(CSV)
                .readAll()
        }

        actual shouldContainExactly listOf(FooWithValueClass("bar1", BazValueClass("baz1")))
    }

    /**
     * This one is broken!
     */
    @Test
    fun valueClassWithColumnReordering() {
        val actual = shouldNotThrowAny {
            createReader<FooWithValueClass>(true)
                .readValues<FooWithValueClass>(CSV)
                .readAll()
        }

        actual shouldContainExactly listOf(FooWithValueClass("bar1", BazValueClass("baz1")))
    }

    @Test
    fun dataClassWithoutColumnReordering() {
        val actual = shouldNotThrowAny {
            createReader<FooWithDataClass>(false)
                .readValues<FooWithDataClass>(CSV)
                .readAll()
        }

        actual shouldContainExactly listOf(FooWithDataClass("bar1", BazDataClass("baz1")))
    }

    @Test
    fun dataClassWithColumnReordering() {
        val actual = shouldNotThrowAny {
            createReader<FooWithDataClass>(true)
                .readValues<FooWithDataClass>(CSV)
                .readAll()
        }

        actual shouldContainExactly listOf(FooWithDataClass("bar1", BazDataClass("baz1")))
    }
}

Expected behavior

Jackson deserializes the CSV without issues just like with data class fields or with column reordering disabled.

Versions

Kotlin: 1.9.23
Jackson-module-kotlin: 2.17.1
Jackson-databind: 2.17.1

Additional context

No response

@marvk marvk added the bug label May 28, 2024
@cowtowncoder
Copy link
Member

If possible, it'd be great to have Java-only reproduction -- if so, could transfer to jackson-dataformats-text where this belong (but can't if there's Kotlin dependency).

@marvk
Copy link
Author

marvk commented Jun 3, 2024

@cowtowncoder Well, I can only reproduce it with Kotlin Value Classes, so I'm not sure how I would reproduce it in Java 😅 As far as I know, Java doesn't have a similar concept (yet?).

@cowtowncoder
Copy link
Member

I was just hoping to confirm whether this is due to Kotlin Value Class specific details (in which case it might not be CSV-specific, fwtw), or not (in which case it is CSV specific).
In former case issue could remain here (if not CSV specific); in latter case moving to jackson-dataformats-text.
But if both CSV and Kotlin aspects matter, probably needs to move to jackson-integration-tests repo.

@k163377
Copy link
Contributor

k163377 commented Jun 15, 2024

@marvk
Could you please check #651?

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

3 participants