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

MismatchedInputException on single ctor paramater, but not with multiple

Open devxzero opened this issue 4 years ago • 4 comments

Describe the bug Deserialization fails for data class Data(var A: Int), probably because of casing problems, (although I couldn't find what the default behavior should be), which can be fixed by using a lowercase for A, or by using different propertyNamingStrategies, like setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE).

But it strangely doesn't fail for data class Data(var A: Int, var b: Int) and for data class Data(var A: Int, var B: Int) and for data class Data(var a: Int, var B: Int). So when a second constructor parameter is present, the behavior differs unexpectedly. So then somehow, the casing for A is ignored, whereas it isn't when only a single constructor parameter is present.

To Reproduce

internal class ObjectMapperTests {
    private val mapper = ObjectMapper()
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
        .registerModule(KotlinModule())
    val json = """{"A":1,"B":2}"""

    @Test // succeeds
    fun upperAUpperB() {
        data class Data(var A: Int, var B: Int)
        mapper.readValue<Data>(json)
    }
    @Test // succeeds
    fun upperALowerB() {
        data class Data(var A: Int, var b: Int)
        mapper.readValue<Data>(json)
    }

    @Test // succeeds
    fun lowerAUpperB() {
        data class Data(var a: Int, var B: Int)
        mapper.readValue<Data>(json)
    }

    @Test // succeeds
    fun lowerA() {
        data class Data(var a: Int)
        mapper.readValue<Data>(json)
    }

    @Test // succeeds
    fun lowerB() {
        data class Data(var b: Int)
        mapper.readValue<Data>(json)
    }

    @Test // fails
    fun upperA() {
        data class Data(var A: Int)
        mapper.readValue<Data>(json)
    }

    @Test // Fails
    fun upperB() {
        data class Data(var B: Int)
        mapper.readValue<Data>(json)
    }
}

Versions Kotlin: 1.5.10 Jackson-module-kotlin: 2.12.5

Expected behavior If data class Data(var A: Int) would fail (which it does), I would expect data class Data(var A: Int, var B: Int) to also fail, but it does not. So somehow the propertyNamingStrategy seems to differ when a second parameter is present.

Update: Here some more examples of different behavior:

    val mapper = ObjectMapper()
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
        .registerKotlinModule()

    @Test // ok
    fun lowercaseJsonStringProperty() {
        val json = """{"abc":"hi"}"""
        data class Data(val abc: String)
        mapper.readValue<Data>(json)
    }

    @Test // fails
    fun uppercaseJsonStringProperty() {
        val json = """{"Abc":"hi"}"""
        data class Data(val abc: String)
        mapper.readValue<Data>(json)
    }

    @Test // ok
    fun uppercaseJsonIntProperty() {
        val json = """{"Abc":1}"""
        data class Data(var abc: Int)
        mapper.readValue<Data>(json)
    }

If lowercaseJsonStringProperty succeeds and uppercaseJsonStringProperty fails, then you would expect uppercaseJsonIntProperty to also fail, but it does not.

devxzero avatar Sep 01 '21 19:09 devxzero

Well this is interesting…I just closed #499 which is very similar. I thought it was simply odd usage that never worked, but with this report I think this could be a real issue.

dinomite avatar Sep 03 '21 18:09 dinomite

I suspect this could be related to the perennial "1-argument constructors are ambiguous" problem?

Although with NamingStrategy there is also the possibility of problems (from databind) wrt too-late binding of constructor properties. :-/

cowtowncoder avatar Sep 04 '21 22:09 cowtowncoder

This affects all 2.12.x versions

dinomite avatar Sep 06 '21 17:09 dinomite

Got interrupted looking at this, but I think @cowtowncoder is right, this isn't a new issue. I'll look through existing issues that relate to this later (or @jtdev you could take a glance https://github.com/FasterXML/jackson-module-kotlin/issues?q=is%3Aissue+is%3Aopen+1-argument+constructor)

Either way a workaround is to add @JsonProperty("foo") annotations to constructor params.

dinomite avatar Sep 08 '21 10:09 dinomite

As previously confirmed, this is a problem caused by databind, and the same problem can be reproduced with Java only.

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;

public class GitHub498J {
    public static class UpperA {
        private final int A;

        public UpperA(int A) {
            this.A = A;
        }

        public int getA() {
            return A;
        }

        // @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
        public static UpperA creator(@JsonProperty("A") int A) {
            return new UpperA(A);
        }
    }

    @Test
    public void test() throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        UpperA result = mapper.readValue("{\"A\":1}", UpperA.class);

        System.out.println(result.getA());
    }
}

Another workaround is to use @JsonCreator(mode = JsonCreator.Mode.PROPERTIES).

This issue is closed because it is not a kotlin-module issue.

k163377 avatar Jun 04 '23 08:06 k163377