ClassCastException (Class to String) in ComponentType.replace() when merging entities with polymorphic embeddables

Environment

  • Hibernate ORM Version: 7.2.7.Final through 7.2.9.Final (Regression from 7.1.x)

  • JDK Version: 25

  • Framework: Spring Boot 4.0.5 (Kotlin 2.3.0)

Description

When calling EntityManager.merge() on an entity that contains a polymorphic @Embeddable (an embeddable hierarchy using @DiscriminatorColumn and @DiscriminatorValue), Hibernate 7.2 throws a ClassCastException: class java.lang.Class cannot be cast to class java.lang.String.

This is a regression introduced in the 7.2.x line. The identical code and operations work perfectly in Hibernate 7.1.8.Final (and 7.1.21.Final).

Steps to Reproduce

  1. Create an entity with an @Embedded property pointing to a polymorphic embeddable hierarchy.

  2. Define the embeddable hierarchy with @DiscriminatorColumn(name = "...") and subclasses with @DiscriminatorValue.

  3. Fetch an existing entity from the database.

  4. Modify the entity (or leave it as is) and call EntityManager.merge() (e.g., via Spring Data JPA’s save()).

Reproducer Classes (Kotlin):

@Embeddable
@DiscriminatorColumn(name = "publishing_mode")
sealed class Publishing(
    @Enumerated(EnumType.STRING)
    @Column(name = "publishing_mode", insertable = false, updatable = false, nullable = false)
    val mode: PublishingMode,
) {
    @Embeddable
    @DiscriminatorValue("SAMPLE")
    data object Sample : Publishing(PublishingMode.SAMPLE)

    @Embeddable
    @DiscriminatorValue("SAMPLE_VALUE_CHANGED")
    data object SampleValueChanged : Publishing(PublishingMode.SAMPLE_VALUE_CHANGED)

    @Embeddable
    @DiscriminatorValue("INTERVAL")
    data class Interval(
        @Column(name = "publishing_interval")
        val interval: Duration,
    ) : Publishing(PublishingMode.INTERVAL)
}

@Entity
class DatapointTemplate {
    @Id val id: UUID
    @Embedded var publishing: Publishing? = null
    // ...
}

Expected Behavior

The entity should merge successfully, updating the database as it did in Hibernate 7.1.x.

Actual Behavior

The merge operation fails internally with the following stack trace:

java.lang.ClassCastException: class java.lang.Class cannot be cast to class java.lang.String (java.lang.Class and java.lang.String are in module java.base of loader 'bootstrap')
    at org.hibernate.type.descriptor.java.StringJavaType.cast(StringJavaType.java:53)
    at org.hibernate.type.descriptor.java.StringJavaType.cast(StringJavaType.java:26)
    at org.hibernate.type.AbstractStandardBasicType.replace(AbstractStandardBasicType.java:292)
    at org.hibernate.type.TypeHelper.replace(TypeHelper.java:85)
    at org.hibernate.type.ComponentType.replace(ComponentType.java:537)
    at org.hibernate.type.TypeHelper.replace(TypeHelper.java:113)
    at org.hibernate.event.internal.DefaultMergeEventListener.entityIsPersistent(DefaultMergeEventListener.java:268)
    at org.hibernate.event.internal.DefaultMergeEventListener.merge(DefaultMergeEventListener.java:198)
    at org.hibernate.event.internal.DefaultMergeEventListener.doMerge(DefaultMergeEventListener.java:136)

Root Cause Analysis

The issue lies in how Hibernate 7.2 handles the discriminator value internally for polymorphic embeddables during the replace step of a merge.

  1. ComponentType.replace() calls getPropertyValues(original), which delegates to embeddableTypeDescriptor().getValues(component).

  2. For polymorphic embeddables, this returns an array of values where the discriminator value is represented as an internal Class<?> object rather than its mapped string value. (Note: ComponentType.resolve() confirms this behavior with the comment: // the discriminator here is the composite class because it gets converted to the domain type when extracted).

  3. ComponentType.replace() then passes this array to TypeHelper.replace(), which iterates over the propertyTypes.

  4. The discriminator property type expects a String (backed by StringJavaType).

  5. When AbstractStandardBasicType.replace() attempts to process the discriminator, it routes to StringJavaType.cast(), which executes a raw (String) value cast.

  6. Since the value is a Class<?> object and not a String, a ClassCastException is thrown.

The replace() code path does not account for the internal Class<?> representation of embeddable discriminators introduced in recent versions.

Workaround

Downgrading/pinning Hibernate ORM to 7.1.21.Final entirely resolves the issue.

Thank you for the report. Please try to create a reproducer with our test case template and if you are able to reproduce the issue, create a bug ticket in our issue tracker and attach that reproducer.

Thanks for the ino. I created a reproducer and created the issue: Jira