Problem Description
We’re experiencing an issue where Hibernate’s dirty checking for JSON-typed fields (@JdbcTypeCode(SqlTypes.JSON)) incorrectly detects changes during the initial save() operation, causing the @Version field to increment from 0 to 1 immediately after INSERT, even though no actual changes were made.
Environment
-
Hibernate Version: 6.x (via Spring Boot 3.5.8)
-
Database: MySQL 8.0
-
Jackson Version: 2.x (via Spring Boot)
-
JSON Mapping: Using
JacksonJsonFormatMapperwith customObjectMapper
Configuration
We’ve configured a custom ObjectMapper for Hibernate’s JSON handling to exclude null fields during serialization (to reduce database storage size):
@Bean
public HibernatePropertiesCustomizer jsonFormatMapperCustomizer(ObjectMapper objectMapper) {
ObjectMapper hibernateObjectMapper = objectMapper.copy()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
return properties -> properties.put(
MappingSettings.JSON_FORMAT_MAPPER,
new JacksonJsonFormatMapper(hibernateObjectMapper)
);
}
Entity Structure
@Entity
@Table(name = "example_entity")
public class ExampleEntity {
@Id
@Column(name = "id")
private UUID id;
@Version
@Column(name = "version")
private Long version;
@Column(name = "json_data")
@JdbcTypeCode(SqlTypes.JSON)
private Map<String, CustomObject> jsonData; // Contains nested objects with null fields
@Column(name = "metadata")
@JdbcTypeCode(SqlTypes.JSON)
private Map<String, Object> metadata; // Another JSON field
// ... other JSON-typed fields
}
Observed Behavior
-
Expected: After
save(), version should be0 -
Actual: After
save(), version is1
Sequence of Events (from debug logs)
-
PRE_INSERT: Version is 0(correct) -
POST_INSERT: Version is 0(INSERT successful) -
PRE_UPDATE: Hibernate detects change in jsonDatafield -
POST_UPDATE: Version increments to 1(incorrect)
The PRE_UPDATE event shows:
Changed properties: [jsonData, version]
Old version: 0, New version: 0
Root Cause Analysis
We believe the issue occurs because:
-
During INSERT: Hibernate serializes the entity’s JSON fields using
ObjectMapperwithNON_NULL, excluding null fields from the JSON. -
After INSERT: Hibernate may reload the entity from the database to get generated values (like
date_created,date_modified). During this reload:-
Hibernate reads the JSON from the database (which doesn’t contain null fields)
-
Deserializes it using the same
ObjectMapperwithNON_NULL -
Creates a new Java object from the deserialized JSON
-
-
Dirty Checking: Hibernate compares the original state (snapshot taken before INSERT) with the current state (after reload). Even though both serialize to identical JSON bytes, the object graph structure may differ:
-
Original object: Has fields explicitly set to
null(e.g.,def.setAnotherField(null)) -
Deserialized object: Has fields that were never set (they don’t exist in the JSON, so Jackson never called the setter)
-
-
False Positive: Hibernate’s dirty checking detects this structural difference and incorrectly triggers an UPDATE, incrementing the version.
Why NON_NULL Affects Both Serialization and Deserialization
According to this GitHub issue, JsonInclude.Include.NON_NULL can affect deserialization behavior. The JacksonJsonFormatMapper uses the same ObjectMapper instance for both serialization and deserialization, so we cannot separate the behavior.
What We’ve Tried
-
Added
ORDER_MAP_ENTRIES_BY_KEYS: To ensure consistent JSON key ordering (didn’t solve the issue) -
Event Listeners: Attempted to detect and prevent false positive updates in
PreUpdateEventListener, but returningtruedoesn’t seem to veto the update in our Hibernate version -
Documentation Research: Found similar issues but no clear solution for separating serialization/deserialization behavior with
JacksonJsonFormatMapper
Questions
-
Is there a way to configure
JacksonJsonFormatMapperto use differentObjectMapperinstances for serialization and deserialization?- We want
NON_NULLfor serialization (to save space) but standard behavior for deserialization (to avoid structural differences)
- We want
-
Is there a Hibernate configuration to prevent automatic entity reload after INSERT?
- This might prevent the false positive detection
-
Is there a better way to handle JSON field dirty checking that accounts for
NON_NULLserialization?- Perhaps a custom
FormatMapperimplementation?
- Perhaps a custom
-
Has anyone else encountered this issue and found a solution?
-
Is this a known limitation when using
NON_NULLwith Hibernate’s JSON type support?
Additional Context
-
The issue affects multiple JSON-typed fields, not just
jsonData -
We’re using
@Versionfor optimistic locking, so accurate version tracking is important
Minimal Reproducible Example
// Entity
@Entity
public class TestEntity {
@Id
private UUID id;
@Version
private Long version;
@JdbcTypeCode(SqlTypes.JSON)
private Map<String, Object> jsonField; // Contains nested objects with null fields
}
// Test
@Test
void testVersionAfterSave() {
TestEntity entity = new TestEntity();
entity.setJsonField(createJsonWithNulls()); // Some nested fields are null
entity = repository.save(entity);
assertThat(entity.getVersion()).isEqualTo(0L); // ❌ Fails: actual is 1L
}