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

Primitive to record mapping fails to find constructor #3381

Open
dalira opened this issue Jan 22, 2022 · 6 comments
Open

Primitive to record mapping fails to find constructor #3381

dalira opened this issue Jan 22, 2022 · 6 comments
Labels
to-evaluate Issue that has been received but not yet evaluated

Comments

@dalira
Copy link

dalira commented Jan 22, 2022

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 dalira added the to-evaluate Issue that has been received but not yet evaluated label Jan 22, 2022
@yawkat
Copy link
Member

yawkat commented Jan 24, 2022

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

@dalira
Copy link
Author

dalira commented Jan 24, 2022

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

@yawkat
Copy link
Member

yawkat commented Jan 24, 2022

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.

@cowtowncoder
Copy link
Member

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.

@yihtserns
Copy link
Contributor

yihtserns commented Jun 8, 2023

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
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
to-evaluate Issue that has been received but not yet evaluated
Projects
None yet
Development

No branches or pull requests

4 participants