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

Converter doesn't work without @JsonCreator for Records #3747

Open
zman0900 opened this issue Jan 20, 2023 · 14 comments
Open

Converter doesn't work without @JsonCreator for Records #3747

zman0900 opened this issue Jan 20, 2023 · 14 comments
Labels
Record Issue related to JDK17 java.lang.Record support
Milestone

Comments

@zman0900
Copy link

Describe the bug
When annotation like @JsonDeserialize(converter = MyConverter.class) is applied to a component of a Record or to a field of an immutable class (such as lombok @value or just plain class with final fields, all-args constructor, and only getters), it seems the Converter is not actually used unless @JsonCreator is also present on the constructor.

Version information
Jackson Databind 2.14.1 (with parameter names module)
Java 17

To Reproduce
Given converter class like:

public class MyConverter extends StdConverter<String, Long> {
    @Override
    public Long convert(final String value) {
        return // some special conversion of string to long
    }
}

And some JSON like this:

{
  "maybeNumber":"blah",
  "other":"value"
}

I believe Jackson should be able to bind this Record:

public record TestRec(
    @JsonDeserialize(converter = MyConverter.class)
    long maybeNumber,
    String other
) { }

Or bind this class:

@lombok.Value
public class TestVal {
    long maybeNumber;
    String other;
}

But these lead to errors like:

com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `long` from String "blah": not a valid `long` value
 at [Source: (String)"{
 "maybeNumber":"blah",
 "other":"value"
}"; line: 2, column: 15] (through reference chain: something.TestRec["maybeNumber"])
	at com.fasterxml.jackson.databind.exc.InvalidFormatException.from(InvalidFormatException.java:67)
	at com.fasterxml.jackson.databind.DeserializationContext.weirdStringException(DeserializationContext.java:1996)
	at com.fasterxml.jackson.databind.DeserializationContext.handleWeirdStringValue(DeserializationContext.java:1224)
	at com.fasterxml.jackson.databind.deser.std.StdDeserializer._parseLongPrimitive(StdDeserializer.java:916)
	at com.fasterxml.jackson.databind.deser.std.StdDeserializer._parseLongPrimitive(StdDeserializer.java:904)
	at com.fasterxml.jackson.databind.deser.std.NumberDeserializers$LongDeserializer.deserialize(NumberDeserializers.java:573)
	at com.fasterxml.jackson.databind.deser.std.NumberDeserializers$LongDeserializer.deserialize(NumberDeserializers.java:550)
	at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:542)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:564)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:439)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1405)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:352)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185)
	at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:323)
	at com.fasterxml.jackson.databind.ObjectReader._bindAndClose(ObjectReader.java:2105)
	at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:1546)

Adding a compact constructor to the Record (or explicit all-args constructor to the class) that is annotated with @JsonCreator works around this.

public record TestRec(
    @JsonDeserialize(converter = MyConverter.class)
    long maybeNumber,
    String other
) { 
    @JsonCreator
    public TestRec {
    }
}

I don't believe this should be necessary, as using custom deserializer like @JsonDeserialize(using = MyDeserializer.class) seems to work fine without it.

@zman0900 zman0900 added the to-evaluate Issue that has been received but not yet evaluated label Jan 20, 2023
@cowtowncoder
Copy link
Member

I wonder if this fails against latest 2.15.0-SNAPSHOT -- there are a few improvements to Record handling that might solve this issue.

Lombok case may be different: it all depends on how Lombok pre-processes class definitions; although I think POJOs will generally require use of @JsonCreator except in limited special cases such as (one of?) parameters annotated with @JsonProperty (which triggers auto-detection). But even that requires that there are no other possibly competing constructors.

So basically this would be two separate (but related) issues: whether this works for Records (it should), and for POJOs (likely not as there is no definition of "canonical constructor" nor necessarily parameter names).

@zman0900
Copy link
Author

zman0900 commented Jan 21, 2023

I also tried with the de-lomboked version (private final fields, getters, single constructor) and saw the same behavior - had to add @JsonCreator to the constructor. In all cases, it worked fine without @JsonCreator when maybeNumber was just a String field with no annotation, and it did seem like adding @JsonDeserialize(using = ...) to it was working too.

@cowtowncoder cowtowncoder changed the title Converter doesn't work without @JsonCreator for immutable types Converter doesn't work without @JsonCreator for Records Jan 21, 2023
@cowtowncoder
Copy link
Member

I changed the title here to refer to just Records: separate one would be needed for POJOs. Or alternatively may change this to refer to POJOs, file new one for Records if issue is still reproducible. Two cases need to be dealt with separately I think.

@yihtserns
Copy link
Contributor

With the renamed title, it now becomes duplicate of #3297?

@cowtowncoder
Copy link
Member

cowtowncoder commented Feb 2, 2023

@yihtserns Which is, I think, fixed? But yes, will close as duplicate.

EDIT: Actually, will hold off not closing yet.

@cowtowncoder cowtowncoder added 2.15 Record Issue related to JDK17 java.lang.Record support and removed to-evaluate Issue that has been received but not yet evaluated labels Feb 2, 2023
@cowtowncoder cowtowncoder added this to the 2.15.0 milestone Feb 2, 2023
@cowtowncoder
Copy link
Member

@zman0900 There has been a major update with #3724 -- if you have any way to test your code against 2.15.0-SNAPSHOT (either built locally, or from Sonatype Snapshot repo), it'd be good to see if your use case might now work too.
It should, I think, as Record constructor discovery was improved significantly.

@yihtserns
Copy link
Contributor

Which is, I think, fixed? But yes, will close as duplicate.

Yupe I've tested with com.fasterxml.jackson.core:jackson-databind:2.15.0-SNAPSHOT from https://oss.sonatype.org/content/repositories/snapshots, and it worked.

But yeah @zman0900 can also verify that it works on his side, before closing this.

@yihtserns
Copy link
Contributor

yihtserns commented Feb 3, 2023

@zman0900 about the issue with Lombok, I think you should create another issue for that discussion as suggested because something weird and very different is going on over there.

Please drop the link here once you've created it, so I can also include what I've discovered so far. (been long enough that I've forgotten what I've discovered).

@lbkulinski
Copy link

Hello! I am actually facing this issue when used in conjunction @JsonAlias. For example, with:

@JsonAlias("isApp")
@JsonDeserialize(converter = StringToBooleanConverter.class)
boolean due

I receive the following exception:

com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `boolean` from String "0": only "true"/"True"/"TRUE" or "false"/"False"/"FALSE" recognized

When I use @JsonProperty instead of @JsonAlias, it works fine. I am using jackson-databind 2.14.2. When I use the 2.15.0 RC, I get some weird exceptions:

java.lang.NoSuchMethodError: 'com.fasterxml.jackson.core.StreamReadConstraints com.fasterxml.jackson.core.JsonParser.streamReadConstraints()'
	at com.fasterxml.jackson.databind.deser.std.NumberDeserializers$BigDecimalDeserializer.deserialize(NumberDeserializers.java:1037) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.std.NumberDeserializers$BigDecimalDeserializer.deserialize(NumberDeserializers.java:976) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:545) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:568) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:439) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1409) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:352) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer._deserializeFromArray(CollectionDeserializer.java:359) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:244) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:28) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:545) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:568) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:439) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1409) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:352) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:545) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:568) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:439) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1409) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:352) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:323) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.ObjectReader._bindAndClose(ObjectReader.java:2105) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:1481) ~[jackson-databind-2.15.0-rc2.jar:2.15.0-rc2]
	at org.springframework.http.codec.json.AbstractJackson2Decoder.decode(AbstractJackson2Decoder.java:204) ~[spring-web-6.0.7.jar:6.0.7]
	at org.springframework.http.codec.json.AbstractJackson2Decoder.lambda$decodeToMono$2(AbstractJackson2Decoder.java:189) ~[spring-web-6.0.7.jar:6.0.7]
	at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:132) ~[reactor-core-3.5.4.jar:3.5.4]
	at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107) ~[reactor-core-3.5.4.jar:3.5.4]
	at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:299) ~[reactor-core-3.5.4.jar:3.5.4]
	at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:337) ~[reactor-core-3.5.4.jar:3.5.4]
	at reactor.core.publisher.Operators$BaseFluxToMonoOperator.completePossiblyEmpty(Operators.java:2071) ~[reactor-core-3.5.4.jar:3.5.4]
	at reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:145) ~[reactor-core-3.5.4.jar:3.5.4]
	at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144) ~[reactor-core-3.5.4.jar:3.5.4]
	at reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:260) ~[reactor-core-3.5.4.jar:3.5.4]
	at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144) ~[reactor-core-3.5.4.jar:3.5.4]
	at reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:415) ~[reactor-netty-core-1.1.5.jar:1.1.5]
	at reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:431) ~[reactor-netty-core-1.1.5.jar:1.1.5]
	at reactor.netty.channel.ChannelOperations.terminate(ChannelOperations.java:485) ~[reactor-netty-core-1.1.5.jar:1.1.5]
	at reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:712) ~[reactor-netty-http-1.1.5.jar:1.1.5]

To add some context, the API I am using returns 0 for false and 1 for true. I'd like to map that to a boolean, hence my use of a converter. Not sure if there is a better way to do this currently in Jackson.

@yihtserns
Copy link
Contributor

yihtserns commented Mar 31, 2023

@lbkulinski I'm not able to reproduce your issue when using 2.15.0-rc1 nor 2.15.0-rc2.

I recommend that you create a new issue, together with a sample Github repo that reproduces that issue.

@lbkulinski
Copy link

@lbkulinski I'm not able to reproduce your issue when using 2.15.0-rc1 nor 2.15.0-rc2.

I recommend that you create a new issue, together with a sample Github repo that reproduces that issue.

Will do. Thanks!

@yihtserns
Copy link
Contributor

@lbkulinski (From a quick glance, I suspect that error is due to version of jackson-core not matching version of jackson-databind, e.g. you're using jackson-databind:2.15.0-rc1 with jackson-core:2.14.2)

@cowtowncoder
Copy link
Member

Yes, that exceptions is due to mismatch: jackson-databind 2.15 depends on some new features of jackson-core 2.14 (reverse would typically not be a problem -- databind 2.14 would work with core 2.15, but it is recommended to match minor versions of all components for simplicity).

@lbkulinski
Copy link

Yep. That was it! I had just been using jackson-core from spring-boot-starter-parent. Thanks all!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Record Issue related to JDK17 java.lang.Record support
Projects
None yet
Development

No branches or pull requests

4 participants