hibernate/hibernate-orm · error · WrongClassException
Expected object of type `%s`, but found `%s`; discriminator
Error message
Expected object of type `%s`, but found `%s`; discriminator = %s
What it means
When an entity initializer reads a discriminator to pick the concrete persister (resolveConcreteDescriptor), it calls discriminatorMapping.resolveDiscriminatorValue(discriminator) and then checks indicatedEntity.isTypeOrSuperType(entityDescriptor). If the class the discriminator indicates is not the expected type or a subtype of it, Hibernate throws WrongClassException('Expected object of type X, but found Y; discriminator = <value>'). The row claims an inheritance type that does not fit where the row was found.
Source
Thrown at hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java:1054
}
else {
assert entityDescriptor.hasSubclasses()
: "Reading a discriminator from a result set should only happen if the entity has subclasses";
final var discriminatorMapping = entityDescriptor.getDiscriminatorMapping();
assert discriminatorMapping != null;
final Object discriminator = discriminatorAssembler.extractRawValue( rowProcessingState );
final var discriminatorDetails = discriminatorMapping.resolveDiscriminatorValue( discriminator );
if ( discriminatorDetails == null ) {
assert discriminator == null : "Discriminator details should only be null for null values";
return null;
}
else {
final var indicatedEntity = discriminatorDetails.getIndicatedEntity();
if ( indicatedEntity.isTypeOrSuperType( entityDescriptor ) ) {
return indicatedEntity.getEntityPersister();
}
else {
throw new WrongClassException(
indicatedEntity.getEntityName(),
null,
entityDescriptor.getEntityName(),
discriminator
);
}
}
}
}
protected boolean useEmbeddedIdentifierInstanceAsEntity(EntityInitializerData data) {
if ( data.canUseEmbeddedIdentifierInstanceAsEntity ) {
data.concreteDescriptor =
determineConcreteEntityDescriptor( data.getRowProcessingState(),
discriminatorAssembler, entityDescriptor );
return data.concreteDescriptor != null
&& data.concreteDescriptor.isInstance( data.getRowProcessingState().getEntityId() );
}View on GitHub (pinned to fad1729dce)
Solutions
- Compare the reported discriminator value with your @DiscriminatorValue mapping and UPDATE the rows to a valid, matching value.
- If the row legitimately belongs to a different branch, correct the declared association type to the common superclass of all possible targets.
- After renaming entities/discriminator values, run a data migration (UPDATE ... SET dtype = 'NewName' WHERE dtype = 'OldName').
- Verify the inheritance mapping (@Inheritance(strategy=...), @DiscriminatorColumn) matches how the data was written.
Example fix
-- before: rows carry a discriminator Hibernate cannot map onto the expected hierarchy UPDATE document SET dtype = 'CONTRACT' WHERE dtype = 'AGR_CONTRACT'; // before (association declared too narrow for the data present) @ManyToOne(targetEntity = Contract.class) private Contract document; // after (declare the branch the data actually contains, or the common base) @ManyToOne(targetEntity = LegalDocument.class) private LegalDocument document;
Defensive patterns
Strategy: validation
Validate before calling
// Compare distinct discriminator values in the DB against the mapped ones
Map<String, Class<?>> allowed = Map.of("DOG", Dog.class, "CAT", Cat.class); // your @DiscriminatorValues
List<Object> distinct = em.createNativeQuery("select distinct dtype from animal").getResultList();
for (Object d : distinct) {
if (d == null || !allowed.containsKey(d.toString())) {
throw new IllegalStateException("Unknown discriminator value in animal: " + d);
}
} Type guard
// Narrow loaded polymorphic values before use
if (document instanceof Contract c) { ... } // pattern-match the concrete branch
boolean fitsExpectedBranch(Class<?> rowType, Class<?> expected) {
return expected.isAssignableFrom(rowType);
} Try / catch
try {
List<LegalDocument> docs = em.createQuery("select d from LegalDocument d", LegalDocument.class).getResultList();
} catch (WrongClassException e) {
// e.getExpectedClassName() / e.getActualClassName() / e.getDiscriminator()
// quarantine the row or widen the declared association type
} Prevention
- Migrate discriminator values whenever you rename entities or @DiscriminatorValue strings.
- Add a CHECK constraint or lookup table on the discriminator column.
- Assert association target types cover every discriminator branch the data actually contains.
- Integration-test polymorphic loading with every subclass row present.
When it happens
Trigger: A polymorphic association declared against class A, but the row's discriminator denotes class B outside A's hierarchy branch; discriminator strings in the DB not matching any @DiscriminatorValue (e.g. after renaming entities or discriminator values); manually inserted rows with a wrong discriminator; joined-inheritance data where subclass rows and discriminators disagree.
Common situations: Refactoring or renaming entity names / @DiscriminatorValue strings without migrating existing rows; multi-module apps where one module inserts rows with discriminators another module's mapping does not know; copy-pasted data between environments; adding a new subclass to one deployment while older rows reference a removed branch.
Related errors
- Two different subclasses of '" + getEntityName() + "' map to
- Cannot interpret discriminator value ({discriminatorRole}) :
- Illegal discriminator type: {discriminatorType}
- Could not parse discriminator value '{discriminatorValue}' a
- Cannot convert NOT_NULL discriminator marker to a relational
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/4d26ef53b2632640.
Report an issue: GitHub.