Skip to content

Jackson3 Deserialization with Properties Based constructor fails with tools.jackson.databind.exc.InvalidDefinitionException #5593

@buksvdl

Description

@buksvdl

Search before asking

  • I searched in the issues and found nothing similar.

Describe the bug

Deserialization fails with InvalidDefinitionException: Cannot construct instance of AuditEvent (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)

Version Information

3.0.3

Reproduction

The Test attempts to serialize and deserialize the AuditEvent class described below but fails on Deserialization with:
tools.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of AuditEvent` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (String)"{"timestamp":"2026-01-17T21:00:44.532975Z","principal":"user","type":"type","data":{"keyA":"ValueA","KeyB":"ValueB"}}"; line: 1, column: 2]

at tools.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:70)
at tools.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1950)
at tools.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:448)
at tools.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1480)
at tools.jackson.databind.deser.bean.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1432)
at tools.jackson.databind.deser.bean.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:480)
at tools.jackson.databind.deser.bean.BeanDeserializer.deserialize(BeanDeserializer.java:200)
at tools.jackson.databind.deser.DeserializationContextExt.readRootValue(DeserializationContextExt.java:265)`

Class for Serialization/Deserialization tests:

/**
 * Copied from Spring Boot 4.0.0 for testing purposes.
 * {@link org.springframework.boot.actuate.audit.AuditEvent}
 * Simplified version for Jackson3 Deserializer issues.
 */
public class AuditEvent implements Serializable {
    
    private final Instant timestamp;    
    private final String principal;    
    private final String type;    
    private final Map<String, @Nullable Object> data;
    
    /**
     * Create a new audit event for the current time from data provided as name-value
     * pairs.
     * @param principal the user principal responsible
     * @param type the event type
     * @param data the event data in the form 'key=value' or simply 'key'
     */
    public AuditEvent(@Nullable String principal, String type, String... data) {
        this(Instant.now(), principal, type, convert(data));
    }
    
    /**
     * Create a new audit event.
     * @param timestamp the date/time of the event
     * @param principal the user principal responsible
     * @param type the event type
     * @param data the event data
     */
    public AuditEvent(Instant timestamp, @Nullable String principal, String type, Map<String, @Nullable Object> data) {
        Assert.notNull(timestamp, "'timestamp' must not be null");
        Assert.notNull(type, "'type' must not be null");
        this.timestamp = timestamp;
        this.principal = (principal != null) ? principal : "";
        this.type = type;
        this.data = Collections.unmodifiableMap(data);
    }
    
    private static Map<String, @Nullable Object> convert(String[] data) {
        Map<String, @Nullable Object> result = new HashMap<>();
        for (String entry : data) {
            int index = entry.indexOf('=');
            if (index != -1) {
                result.put(entry.substring(0, index), entry.substring(index + 1));
            }
            else {
                result.put(entry, null);
            }
        }
        return result;
    }
............................

The Test:

@ActiveProfiles("test")
@SpringJUnitConfig({JacksonConfig.class})
@WebMvcTest
public class AuditEventDeserializeMvcItVal {
    
    @Autowired
    private JsonMapper jsonMapper;
    
    private AuditEvent startAuditEvent;
    private String startAuditEventJson;
    private String startAuditEventString;
    
    private JsonMapper jsonMapperManual;
    
    @BeforeEach
    void serialize(){
        
        startAuditEvent = new AuditEvent("user", "type", "keyA=ValueA","KeyB=ValueB");
        startAuditEventJson = jsonMapper.writeValueAsString(startAuditEvent);
        startAuditEventString = startAuditEvent.toString();
        System.out.println("Starting AuditEvent Json: %s".formatted(startAuditEventJson));
        System.out.println("Starting AuditEvent String: %s".formatted(startAuditEventString));
        
        jsonMapperManual =
            JsonMapper
                .builder()
                .enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION)
                .constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED)
                .build();
    }
    
    @Test
    void deserializeWithSpringMapper() {
        
        AuditEvent auditEvent =
            jsonMapper.readValue(
                startAuditEventJson,
                AuditEvent.class);
        
        Assertions.assertNotNull(auditEvent);
        Assertions.assertEquals("user", auditEvent.getPrincipal());
        Assertions.assertEquals("type", auditEvent.getType());
        Assertions.assertEquals(2, auditEvent.getData().size());
    }
    
    @Test
    void deserializeWithLocalMapperDef() {
        
        AuditEvent auditEvent =
            jsonMapperManual.readValue(
                startAuditEventJson,
                AuditEvent.class);
        
        Assertions.assertNotNull(auditEvent);
        Assertions.assertEquals("user", auditEvent.getPrincipal());
        Assertions.assertEquals("type", auditEvent.getType());
        Assertions.assertEquals(2, auditEvent.getData().size());
    }
}

Expected behavior

AuditEvent should be constructed as a public constructor for the properties supplied exists.

Additional context

The defaut builder(which was always enough in Jackson2) fails but I have a Spring config which fails as well.

@Bean
    public JsonMapperBuilderCustomizer includeSourceInLocationCustomizer() {
        return builder -> {
            builder.enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION);
            builder.constructorDetector(
                ConstructorDetector.USE_PROPERTIES_BASED
                    .withAllowImplicitWithDefaultConstructor(ConstructorDetector.DEFAULT_ALLOW_IMPLICIT_WITH_DEFAULT_CONSTRUCTOR)
                    .withRequireAnnotation(false)
                    .withAllowJDKTypeConstructors(true)
                    .withSingleArgMode(ConstructorDetector.SingleArgConstructor.PROPERTIES));
        };
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    need-test-caseTo work on issue, a reproduction (ideally unit test) needed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions