jackson-databind: Regression: 2.15.0 breaks deserialization for records when mapper.setVisibility(PropertyAccessor.ALL, Visibility.NONE);

Describe the bug This code used to work with 2.14.2, but not with 2.15.0

ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, Visibility.NONE);
RecordTest recordTest_deserialized = mapper.readValue("{}", RecordTest.class);

Version information 2.15.0

To Reproduce

/**
 * This works fine with Jackson 2.14.2, but not with 2.15.0.
 */
public class Jackson_2_15_0_Regression {

    record RecordTest(String string, int integer) {
    }

    @Test
    public void emptyJsonToRecord() throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();

        mapper.setVisibility(PropertyAccessor.ALL, Visibility.NONE);
        // mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);

        RecordTest recordTest_deserialized = mapper.readValue("{}", RecordTest.class);


        System.out.println("RecordTest deserialized: " + recordTest_deserialized);
        Assert.assertEquals(new RecordTest(null, 0), recordTest_deserialized);
    }
}

Comment out the mapper.setVisibility(PropertyAccessor.ALL, Visibility.NONE) and it works.

Note that the commented-out mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY) is included in my actual code to get intended behaviour (i.e. only fields are serialized and deserialized, no methods involved), but this did not make any to or from for the problem.

Exception is:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `io.mats3.examples.jbang.Jackson_2_15_0_Regression$RecordTest` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (String)"{}"; line: 1, column: 2]

	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
	at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1915)
	at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:414)
	at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1360)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1424)
	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.ObjectMapper._readMapAndClose(ObjectMapper.java:4825)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3772)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3740)
	at io.mats3.examples.jbang.Jackson_2_15_0_Regression.emptyJsonToRecord(Jackson_2_15_0_Regression.java:25)

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 30 (20 by maintainers)

Commits related to this issue

Most upvoted comments

Do you mean that this won’t be solved “natively”? I must change the code?

Need to wait for the core maintainers to make the final decision.

1: Will this work identical for 2.14 and 2.15? Why the Visibility.NON_PRIVATE? I want the no-args to be private if it needs be there. (Just curious: Why is a constructor called a creator, if this is what we’re talking about?)

Creators are constructors or factory methods (with name valueOf) that have parameters, that will be used for deserialization - they are typically either annotated with @JsonCreator or “magically” chosen.

They are not related to, and nor will that config affects no-arg constructor.

2: Hmmm… This needs to be different between 2.14 and 2.15? How will this affect standard class-based POJO DTOs, which might have a bunch of constructors that I do not want the serialization to touch?

You can use this with both 2.14 & 2.15, but it basically does nothing for 2.14 (because again, deserialization was implemented differently). This is basically like number 1, except it only targets Record classes:

return ac.getType().isRecordType()                          // if we're deserializing Record class...
    ? checker.withCreatorVisibility(Visibility.NON_PRIVATE) // ...make the Creators visible
    : checker;                                              // ...leave the visibility as NONE for POJO types

As for changing Jackson to make Records’ creators always visibility regardless of config, I can/will create a PR if the core maintainers think that’s the way to go.

Yes, but I tried to mention:

Note that the commented-out mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY) is included in my actual code to get intended behaviour (i.e. only fields are serialized and deserialized, no methods involved), but this did not make any to or from for the problem.

It was an attempt to reduce the problem to the bare minimum.

@yihtserns:

is that what was done in your codebase?

No, as I said at top, I have both of:

ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, Visibility.NONE);
mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);

The full init is:

ObjectMapper mapper = new ObjectMapper();

// Read and write any access modifier fields (e.g. private)
mapper.setVisibility(PropertyAccessor.ALL, Visibility.NONE);
mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);

// Drop nulls
mapper.setSerializationInclusion(Include.NON_NULL);

// If props are in JSON that aren't in Java DTO, do not fail.
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

// Write e.g. Dates as "1975-03-11" instead of timestamp, and instead of array-of-ints [1975, 3, 11].
// Uses ISO8601 with milliseconds and timezone (if present).
mapper.registerModule(new JavaTimeModule());
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);

// Handle Optional, OptionalLong, OptionalDouble
mapper.registerModule(new Jdk8Module());

It is here: https://github.com/centiservice/mats3/blob/main/mats-serial-json/src/main/java/io/mats3/serial/json/MatsSerializerJson.java#L120-L152 - now also with the attempt at handling the 5M chars-in-String limit.

Note: I had accepted that I always need a no-args constructor (as opposed to GSON). (Note: If it is possible to support missing no-args constructor for a class, even with Java 17+, that would be excellent!)

the issue you’re facing now is because Records & POJOs are handled similarly…

Exactly. Which is why I said: “yes, I agree, which makes me wonder a bit about this plan of jamming records and POJOs into the same regime, as seems suggested by @yihtserns?”

My point is that the construction of a Record and of a Class, and their population of fields, are rather different, so it sounds … ambitious … to do it with the same codebase/flow? Note, I do not try to dictate anything here, it is just an observation. But it would be nice if this - as seen from my side - regression - was not present!

One thing worth noting: Records are technically very different from POJOs/Beans, so there are challenges from Jackson implementation side. Especially regarding access to Record Fields; something that will start failing on newer JDKs. It is not certain handling can be fully unified.

I agree that it is important to be able to have value types, handling that works for both 2.14 and 2.15. But we really did not realize that there was Record usage that relied on explicit Visibility settings – there’s no testing so one can view at as unsupported use case, technically. Assumption rather was that usage wrt Records would use default visibility settings.