Describe the bug
I experience a potential regression since 2.12.0 when trying to deserialize a scalar value to a single-value Record. Consider this test case:
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
class RecordStringDeserializationTest {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Test
void deserializeMyValueRecordFromPrimitiveValue() throws Exception {
assertEquals(new MyValueRecord("foo"), OBJECT_MAPPER.readValue("\"foo\"", MyValueRecord.class));
}
final record MyValueRecord(@JsonProperty String value) {}
}
This works as expected in 2.11.3, since 2.12.0 a MismatchedInputException is thrown:
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `us.byteb.jackson.RecordStringDeserializationTest$MyValueRecord` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('foo')
at [Source: (String)""foo""; line: 1, column: 1]
This still works as expected if an explicit factory method annotated with @JsonCreator is provided for the Record or for an equivalent Class without explicit @JsonCreator (see additional test cases). I would expect the behavior for Records to be the same as for Classes, it also seems contrary to the brevity of Records to have to define an additional factory method.
Version information
2.12.0
To Reproduce
See test case above.
Ok so this is a common stumbling block: you are assuming that constructors would be taken as "delegating" creator, for 1-argument case. Unfortunately:
@JsonProperty for parameter explicitly indicates that this should be properties-based case (even with POJOs)So this is working as intended from my perspective.
To use delegating style you will need to add @JsonCreator(mode = JsonCreator.Mode.DELEGATING) annotation on constructor.
However... it does not look like there is a proper test for verifying ability to use delegation so I will add one to verify it actually works. :)
And what do I know: the test case fails. So somehow constructor annotation is not being found; possibly same as or related to #2974.
... except that my test was faulty; when fixed, works as expected. Should always double-check tests when they fail.
Ok, so: I think this is working as expected: by default, Records will use "properties"-style construction.
It is unfortunate that behavior was different in 2.11, as auto-detection and heuristics determined that "delegating" style would be most appropriate (as Record handling was using default POJO logics), but this is the intended behavior with explicit Record support.
To use delegating-style with Records on 2.12 and later, necessary annotation will be:
record MyValueRecord(String value) {
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
public MyValueRecord(String value) {
this.value = value;
}
}
since there is no shorter way to annotate primary constructor (as far as I know).
Thanks very much for answering this in great depth :bouquet:
This makes sense and is still easy and consistent to use. I was only confused by the changed heuristic behavior between versions.
@bfncs Yeah, this wasn't something I had thought of until you reported it; I did not realize Records were already used, although it was mentioned as something that sort of works (when discussing initially addition of explicit support for 2.12).
I will add a note on 2.12 release page: thank you for bringing this up.
One more thing just now came to my mind: this also works with a record's compact constructor and is a bit less boilerplate:
record MyValueRecord(String value) {
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
public MyValueRecord {}
}
Interesting, did not know such shortcut was available. Thank you for sharing!