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

Do not try to set @JsonUnwrapped fields if using a @JsonCreator constructor

Open victornoel opened this issue 7 years ago • 14 comments

Hi,

After discussing this matter on the mailing list, I was redirected to creating an issue to ease some use case of @JsonUnwrapped.

Basically, my objective is to avoid using mutable objects and exploit as much as possible constructors to construct objects.

Taking this example:

class Location {
    public final int latitude, longitude;

    public Location(int latitude, int longitude) { ... }
}

class Place {
    public final String name;
    @JsonUnwrapped public final Location location;

    @JsonCreator
    public Place(@JsonProperty("name") String name,
                 @JsonProperty("latitude") int latitude, 
                 @JsonProperty("longitude") int longitude) {
        this.name = name;
        this.location = new Location(latitude, longitude);
    }
}

Currently, deserializing a Place object fails because Jackson first builds the Place object (successfully, including the location with the correct values) and then tries to set the location field. The latter fails because the latitude and longitude properties were already used to construct the Place, and thus are not used to construct the Location. At the end of the deserialization process, the location field is initialised with an object with null values for latitude and longitude!

I would expect Jackson not to try to set the location field after a Place object was successfully constructed since a constructor is expected to build fully constructed objects?

Apparently (according to the discussion on the mailing list), the following code should have worked:

class Location {
    public final int latitude, longitude;

    public Location(int latitude, int longitude) { ... }
}

class Place {
    public final String name;
    @JsonUnwrapped public final Location location;

    @JsonCreator
    public Place(@JsonProperty("name") String name) {
        this.name = name;
        this.location = null;
    }
}

I.e., the Place object is partially created, and then Jackson would set the location field. In practice it didn't work, not sure why. But with respect to my proposed solution to the first problem of this issue, I'm not sure it is a so good idea that this second way of doing things work: for me a constructor is meant to fully build an object!

FYI, the only solution I found to work is to have a parameter-less constructor and let Jackson handle everything its way. I feel there is some use case where a constructor would really be needed, for example if the properties must be reworked a bit before being set to the object (keeping in mind I don't want to use setters :).

victornoel avatar Jan 12 '17 11:01 victornoel

As Tatu Saloranta noted in the mailing list, an alternative solution to this kind of problem would be to be able to specify that the @JsonUnwrapped annotation only applies to serialization.

victornoel avatar Jan 13 '17 08:01 victornoel

Possible solution that I found to make @JsonUnwrap affect only serialisation, and be able to provide custom @JsonCreator is to add @JsonProperty(access = READ_ONLY) annotation to the "unwrapped" properties.

dbolotin avatar May 10 '17 21:05 dbolotin

@dbolotin so the original example (the one in the issue description) works if you add @JsonProperty(access = READ_ONLY) to location?

I will try to do some tests, thanks for the idea :)

victornoel avatar May 11 '17 07:05 victornoel

Yes, the original example should work as expected. I have very similar situation and @JsonProperty(access = READ_ONLY) in my case prevented Jackson from doing it's magic of modifying final fields after normal construction with @JsonCreator.

dbolotin avatar May 11 '17 07:05 dbolotin

Excellent, thanks again!

victornoel avatar May 11 '17 07:05 victornoel

@victornoel, are you aware of https://github.com/FasterXML/jackson-modules-java8/tree/master/parameter-names?

lpandzic avatar Jul 20 '17 11:07 lpandzic

@lpandzic I am not actually, thanks :) But I don't see how it is related to this issue, could you expand? Thanks!

victornoel avatar Jul 20 '17 11:07 victornoel

It's not, sorry for digressing. I was looking for solution to this issue and saw that you were defining @JsonCreator and @JsonProperty which you can avoid with parameter names module.

lpandzic avatar Jul 20 '17 13:07 lpandzic

@lpandzic ah ok, anyway, thanks for the information, it's useful :)

victornoel avatar Jul 20 '17 14:07 victornoel

Hello, any updates on this issue?

I have the similar problem, but with pure java, not kotlin. There is a little example with migration 2.8 -> 2.9 problem link

slavonic-suomi avatar May 28 '18 13:05 slavonic-suomi

@JsonUnwrapped also does not work for deserializing properties in record classes, since those are final.

phraktle avatar Aug 04 '21 09:08 phraktle

@cowtowncoder any suggestions on a workaround to make @JsonUnwrapped work with record classes?

phraktle avatar Oct 07 '21 11:10 phraktle

@cowtowncoder can I offer any help in fixing this bug?

phraktle avatar Apr 08 '22 14:04 phraktle

@phraktle Help always appreciated!

One practical note: I am leaving on a vacation today, back in 10 days -- so will not be checking for updates. Can sync after that.

cowtowncoder avatar Apr 08 '22 16:04 cowtowncoder

any suggestions on a workaround to make @JsonUnwrapped work with record classes? https://github.com/FasterXML/jackson-databind/issues/1497#issuecomment-937714547

aowss avatar Dec 19 '22 00:12 aowss

To request support for @JsonUnwrapped for record classes, please file a separate issue, with a simple example of kind of usage you are thinking of. I think this issue is for slightly different problem.

cowtowncoder avatar Dec 21 '22 02:12 cowtowncoder