jackson-databind
jackson-databind copied to clipboard
Primitive to record mapping fails to find constructor
Describe the bug Jackson not able to use same deserialization strategy in records that uses in classes.
Version information 2.13.1
To Reproduce
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.junit.jupiter.api.Test;
public class RecordMapperTest {
@Test
void testOnRecord() throws JsonProcessingException {
final var om = new JsonMapper();
om.readValue("1", ProviderRecord.class);
}
@Test
void testOnClass() throws JsonProcessingException {
final var om = new JsonMapper();
om.readValue("1", ProviderClass.class);
}
}
record ProviderRecord(Long refId) {
}
class ProviderClass {
private final Long refId;
public ProviderClass(Long refId) {
this.refId = refId;
}
}
The second test will fail with the message:
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of
com.carepay.visitservice.ProviderRecord (although at least one Creator exists): no int/Int-argument constructor/factory method to deserialize from Number value (1)
Expected behavior It was to expect that the same behavior to deserialize using only the constructor would be possible in the records as it happens on the class.
Additional context Compact or Canonical record constructors, as well as the record visibility, makes no difference in the outcome.
Records are not just treated as normal classes by jackson. e.g. the getter names are different, and property names for the constructor are known.
If you want the same behavior as a normal class, you can annotate the constructor explicitly:
record ProviderRecord(Long refId) {
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
ProviderRecord {}
}
Hi @yawkat
Thank you very much for the very fast response. Appreciated the way you proposal to unblock my issue, will use for now.
But just to understand, for records, there is no way of the deserialization flow to work without the @JsonCreator annotation? I tried for instance using .constructorDetector(ConstructorDetector.USE_DELEGATING) on the JsonMapper but also didn't worked.
I'm just trying to have a notion if what I'm talking is a bug indeed or, if is in fact the intended way of working, if I should open a new feature request.
Best regards
I can't answer authoritatively whether this should be a feature or anything – I don't work on jackson-databind, I just try to help with the issue reports a bit.
constructorDetector
probably doesn't work because records are a different code path to normal constructor detection. Records are essentially treated as named tuples when no annotations are present, because that's one thing they're designed for in the initial JEP. They even have special reflection APIs for that purpose.
Yes, the default assumption is --based on typical Record
usage -- that constructor use "properties-based" approach, and not delegating. So in this case annotation is required.
As to ConstructorDetector
working, that's an interesting question. Right now I think it only applies to POJOs, since I did not think there was need for Record defaulting. I don't think that behavior can be safely changed, either, given that change in logic would change handling for some users.
It might be possible to extend handling to add separate setting for Record
s, and for that you might want to file a separate request.
Other than that, yes, @JsonCreator
is needed to indicate different mode. And this is considered "feature", not a bug that could be fixed (since defaulting logic changes are very easy way to break existing usage, based on my experience).
I hope this helps.
Starting from 2.15.0 (thanks to #3724 + #3654), you can also use @JsonValue
instead of @JsonCreator(mode = DELEGATING)
, i.e.:
record ProviderRecord(@JsonValue Long refId) {
}
...since if you want to deserialize a scalar value into a Record, naturally you should also want to serialize the Record into scalar value...
BUT if you're only using that Record for deserialization & you really prefer not having to annotate, you can also do this starting from 2.15.0 (thanks to #3724):
record ProviderRecord(Long refId) {
// Jackson will consider method with the name "valueOf" as delegating creator
// GOTCHA: method parameter name (e.g. 'val') must NOT be the same as field name (i.e. 'refId')
public static ProviderRecord valueOf(Long val) {
return new ProviderRecord(val);
}
}