jackson-databind icon indicating copy to clipboard operation
jackson-databind copied to clipboard

Primitive to record mapping fails to find constructor

Open dalira opened this issue 2 years ago • 4 comments

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.

dalira avatar Jan 22 '22 18:01 dalira

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 {}
}

yawkat avatar Jan 24 '22 08:01 yawkat

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

dalira avatar Jan 24 '22 08:01 dalira

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.

yawkat avatar Jan 24 '22 08:01 yawkat

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 Records, 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.

cowtowncoder avatar Jan 25 '22 01:01 cowtowncoder

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...

yihtserns avatar Jun 08 '23 10:06 yihtserns

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);
    }
}

yihtserns avatar Jun 08 '23 13:06 yihtserns