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

public val/var in constructor produces 'No String-argument constructor/factory method to deserialize from String value' #91

Closed
robpridham-bbc opened this issue Oct 2, 2017 · 8 comments

Comments

@robpridham-bbc
Copy link

robpridham-bbc commented Oct 2, 2017

As per FasterXML/jackson-dataformat-xml#254, I have a very similar problem. I'm on 2.9.1. The case is so simple that I'm not sure my usage is correct, but the functioning workaround suggests it's legitimate.

The following should serve as a self-contained example.

package com.robpridham.jsonexample

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.junit.Assert.assertEquals
import org.junit.Test

class DataClassTest {

    data class DataClass1(val name: String, val content: DataClass2)

    data class DataClass2(val content: String)

    private val jsonData = """
        {
            "name": "my name",
            "content": "some value"
        }
        """

    @Test
    fun testJsonParsing() {
        val mapper = jacksonObjectMapper()
        val dataClass1 = mapper.readValue<DataClass1>(jsonData)
        assertEquals(DataClass2("some value"), dataClass1.content)
    }
}

This fails with the following:

com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.robpridham.jsonexample.DataClassTest$DataClass2` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('some value')
 at [Source: (String)"
{
            "name": "my name",
            "content": "some value"
        }
        "; line: 4, column: 24] (through reference chain: com.robpridham.jsonexample.DataClassTest$DataClass1["content"])

	at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
	at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1329)
	at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1031)
	at com.fasterxml.jackson.databind.deser.ValueInstantiator._createFromStringFallbacks(ValueInstantiator.java:370)
	at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createFromString(StdValueInstantiator.java:314)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromString(BeanDeserializerBase.java:1351)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:170)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:161)
	at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:519)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:527)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:416)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1265)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:325)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4001)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3011)
	at com.robpridham.jsonexample.DataClassTest.testJsonParsing(DataClassTest.kt:28)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.intellij.rt.execution.application.AppMainV2.main(AppMainV2.java:131)

If I rework DataClass2 to have private val in the constructor, or indeed no val keyword in its constructor at all, then this works.

@robpridham-bbc robpridham-bbc changed the title val/var in constructor produces 'No String-argument constructor/factory method to deserialize from String value' public val/var in constructor produces 'No String-argument constructor/factory method to deserialize from String value' Oct 2, 2017
@robpridham-bbc
Copy link
Author

robpridham-bbc commented Nov 8, 2017

I debugged this as well, and it appears to occur because code in BasicDeserializerFactory._addExplicitAnyCreator finds a property setter, whereupon it stops, and therefore doesn't collect the string argument constructor.

Therefore in StdValueInstantiator.createFromString(), the value of _fromStringCreator is null and it uses the fallback method.

Hiding the property getters/setters changes this behaviour. I expected adding an empty constructor to the class in question would provide a workaround but it doesn't seem to.

//when a data class exposes a public val, this is true
if (useProps) { 
            SettableBeanProperty[] properties = new SettableBeanProperty[] {
                    constructCreatorProperty(ctxt, beanDesc, paramName, 0, param, injectId)
            };
            creators.addPropertyCreator(candidate.creator(), true, properties);
            return;
        }
//therefore this doesn't get called
        _handleSingleArgumentCreator(creators, candidate.creator(), true, true);

@apatrida
Copy link
Member

Are you intending to use the single string constructor model from Jackson where the string is passed to the constructor that then parses it to set the values? Or are you trying to basically @JsonUnwrapped the object to have its value set from one level higher. If unwrapping you need to add that annotation otherwise it appears you are using the single string constructor which is a special deal. Now, the Kotlin module will only allow the single string constructor if it does not have a parameter that appears to be a property (yours appears to be a property).

I think you are intending to use the unwrapped, or can you provide a sample that isn't ambiguous as to the intent.

@Fleshgrinder
Copy link

Hey @apatrida the former case you described is what I have and I'm running into exactly this issue, minimal example:

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

data class ValueObject(@JsonValue val value: String)

fun main(args: Array<String>) {
    val mapper = jacksonObjectMapper()
    val vo = mapper.readValue<ValueObject>(""""value"""")

    check(vo == ValueObject("value"))
}

Deserialization fails with:

Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `ValueObject` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('value')
 at [Source: (String)""value""; line: 1, column: 1]
	at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
	at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1342)
	at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1031)
	at com.fasterxml.jackson.databind.deser.ValueInstantiator._createFromStringFallbacks(ValueInstantiator.java:371)
	at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createFromString(StdValueInstantiator.java:323)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromString(BeanDeserializerBase.java:1366)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:171)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:161)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4001)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3011)
	at MainKt.main(main.kt:14)

The only workaround I found so far is the following:

import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue

data class ValueObject(@JsonValue val value: String) {
    private companion object {
        @JsonCreator
        @JvmStatic
        fun valueOf(value: String) = ValueObject(value)
    }
}

fun main(args: Array<String>) {
    val mapper = jacksonObjectMapper()
    val vo = mapper.readValue<ValueObject>(""""value"""")

    check(vo == ValueObject("value"))
}

@Fleshgrinder
Copy link

Using @get:JsonValue yields the same result (at least in the latest version). 😎

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

data class ValueObject(@get:JsonValue val value: String)

fun main(args: Array<String>) {
    val mapper = jacksonObjectMapper()
    val vo = mapper.readValue<ValueObject>(""""value"""")

    check(vo == ValueObject("value"))
}

This is definitely much better.

@cowtowncoder
Copy link
Member

2.10.0.pr2 was released last night, with some improvements, so make sure to try with that (fixes will be in official 2.10.0, but not all were in 2.10.0.pr1 nor included in 2.9.x patches).

1 similar comment
@cowtowncoder
Copy link
Member

2.10.0.pr2 was released last night, with some improvements, so make sure to try with that (fixes will be in official 2.10.0, but not all were in 2.10.0.pr1 nor included in 2.9.x patches).

@apatrida
Copy link
Member

@Fleshgrinder your issue is unrelated to this issue, and works with 2.10.x version of the module (at least on the 2.10 branch it passes)

@apatrida
Copy link
Member

apatrida commented Oct 27, 2019

I am wondering if what @robpridham-bbc is doing is instead trying to use the ideas of @JsonUnwrapped which won't currently work since (@cowtowncoder) you get the error:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot define Creator property "content" as @JsonUnwrapped: combination not yet supported

If that is not the case, maybe is intending to use @JsonValue annotation but that succeeds in @Fleshgrinder 's case but not the case of a nested object.

But really I think he just wants the single string constructor to work as-is, so I think this is the solution here:

class Github91Test {

    data class DataClass1(val name: String, val content: DataClass2)

    data class DataClass2 @JsonCreator(mode = JsonCreator.Mode.DELEGATING) constructor (@JsonValue val content: String)

    private val jsonData = """
        {
            "name": "my name",
            "content": "some value"
        }
        """

    @Test
    fun testJsonParsing() {
        val mapper = jacksonObjectMapper()
        val dataClass1 = mapper.readValue<DataClass1>(jsonData)
        assertEquals(DataClass1("my name", DataClass2("some value")), dataClass1)
        assertEquals("{\"name\":\"my name\",\"content\":\"some value\"}", mapper.writeValueAsString(dataClass1))
    }
}

Closing this issue because nothing reported here is an actual bug.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants