# False positive dirty checking with JSON fields when using JsonInclude.Include.NON\_NULL causes incorrect version increment on initial save

**URL:** <https://discourse.hibernate.org/t/false-positive-dirty-checking-with-json-fields-when-using-jsoninclude-include-non-null-causes-incorrect-version-increment-on-initial-save/12061>\
**Category:** Hibernate ORM\
**Created:** [December 24, 2025, 10:25am UTC](https://discourse.hibernate.org/t/false-positive-dirty-checking-with-json-fields-when-using-jsoninclude-include-non-null-causes-incorrect-version-increment-on-initial-save/12061 "2025-12-24T10:25:02Z")\
**Posts on this page:** 3\
**Page:** 1

<div class="post-metadata">

**Author:** ![dnavan](https://avatars.discourse-cdn.com/v4/letter/d/4af34b/32.png) [@dnavan](https://discourse.hibernate.org/u/dnavan)\
**Post date:** [December 24, 2025, 10:25am UTC](https://discourse.hibernate.org/t/false-positive-dirty-checking-with-json-fields-when-using-jsoninclude-include-non-null-causes-incorrect-version-increment-on-initial-save/12061/1 "2025-12-24T10:25:02Z")

</div>

## **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 `JacksonJsonFormatMapper` with custom `ObjectMapper`

### **Configuration**

We’ve configured a custom `ObjectMapper` for Hibernate’s JSON handling to exclude null fields during serialization (to reduce database storage size):

```auto
@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**

```auto
@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**

1. **Expected:** After `save()`, version should be `0`

2. **Actual:** After `save()`, version is `1`

### **Sequence of Events (from debug logs)**

1. ✅ **PRE\_INSERT:** Version is `0` (correct)

2. ✅ **POST\_INSERT:** Version is `0` (INSERT successful)

3. ❌ **PRE\_UPDATE:** Hibernate detects change in `jsonData` field

4. ❌ **POST\_UPDATE:** Version increments to `1` (incorrect)

The PRE\_UPDATE event shows:

```auto
Changed properties: [jsonData, version]
Old version: 0, New version: 0

```

## **Root Cause Analysis**

We believe the issue occurs because:

1. **During INSERT:** Hibernate serializes the entity’s JSON fields using `ObjectMapper` with `NON_NULL`, excluding null fields from the JSON.

2. **After INSERT:** Hibernate may reload the entity from the database to get generated values (like `date_created`, `date_modified`). During this reload:

3. **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:

4. **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](https://github.com/OpenAPITools/jackson-databind-nullable/issues/55), `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**

1. **Added `ORDER_MAP_ENTRIES_BY_KEYS`:** To ensure consistent JSON key ordering (didn’t solve the issue)

2. **Event Listeners:** Attempted to detect and prevent false positive updates in `PreUpdateEventListener`, but returning `true` doesn’t seem to veto the update in our Hibernate version

3. **Documentation Research:** Found similar issues but no clear solution for separating serialization/deserialization behavior with `JacksonJsonFormatMapper`

## **Questions**

1. **Is there a way to configure `JacksonJsonFormatMapper` to use different `ObjectMapper` instances for serialization and deserialization?**

2. **Is there a Hibernate configuration to prevent automatic entity reload after INSERT?**

3. **Is there a better way to handle JSON field dirty checking that accounts for `NON_NULL` serialization?**

4. **Has anyone else encountered this issue and found a solution?**

5. **Is this a known limitation when using `NON_NULL` with Hibernate’s JSON type support?**

## **Additional Context**

- The issue affects multiple JSON-typed fields, not just `jsonData`

- We’re using `@Version` for optimistic locking, so accurate version tracking is important

## **Minimal Reproducible Example**

```auto
// 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
}

```

---

<div class="post-metadata">

**Author:** ![beikov](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.hibernate.org/beikov/32/258_2.png) [@beikov](https://discourse.hibernate.org/u/beikov)\
**Post date:** [December 24, 2025, 10:28am UTC](https://discourse.hibernate.org/t/false-positive-dirty-checking-with-json-fields-when-using-jsoninclude-include-non-null-causes-incorrect-version-increment-on-initial-save/12061/2 "2025-12-24T10:28:09Z")

</div>

This has been asked a couple of times already. See e.g. [Extra Update during Insert of Entity with JsonField@JdbcTypeCode(SqlTypes.JSON) property](https://discourse.hibernate.org/t/extra-update-during-insert-of-entity-with-jsonfield-jdbctypecode-sqltypes-json-property/11908)

Please compare the output of the `FormatMapper.fromString( FormatMapper.toString() )` to see if the objects equal, since dirty checking is based on that.

---

<div class="post-metadata">

**Author:** ![beikov](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.hibernate.org/beikov/32/258_2.png) [@beikov](https://discourse.hibernate.org/u/beikov)\
**Post date:** [December 29, 2025, 10:19am UTC](https://discourse.hibernate.org/t/false-positive-dirty-checking-with-json-fields-when-using-jsoninclude-include-non-null-causes-incorrect-version-increment-on-initial-save/12061/4 "2025-12-29T10:19:03Z")

</div>

I appreciate that you’re trying to give me a lot of context so I don’t have to ask many follow up questions, but this AI generated write-ups are tiring to read as they are very long. Sorry for not going into every detail that you present here, but the bottom line is, Hibernate ORM is not detecting false positives, but rather you are causing this problem with your configuration.  
I don’t understand why that listener would be necessary. My guess is that you’re simply missing a proper equals implementation in one of your classes.

You could try to set a breakpoint in `org.hibernate.persister.entity.DirtyHelper#findDirty` to see which field causes Hibernate ORM to think that your entity is dirty and then try to understand further what you may be missing.
