FasterXML / jackson-databind

General data-binding package for Jackson (2.x): works on streaming API (core) implementation(s)
Apache License 2.0
3.52k stars 1.38k forks source link

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

Open zman0900 opened 1 year ago

zman0900 commented 1 year ago

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.

cowtowncoder commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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

cowtowncoder commented 1 year ago

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

EDIT: Actually, will hold off not closing yet.

cowtowncoder commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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