hibernate/hibernate-orm · error · AnnotationException
Property '${property}' belongs to an entity subclass and may
Error message
Property '${property}' belongs to an entity subclass and may not be annotated '@NaturalId' (only a property of a root '@Entity' or a '@MappedSuperclass' may be a '@NaturalId') What it means
@NaturalId marks immutable (or explicitly mutable) business keys and is only supported on properties declared on the root @Entity or on a @MappedSuperclass. When the annotated property belongs to an entity subclass in an inheritance hierarchy, Hibernate rejects it because natural-id columns/unique keys are only defined at the root table level.
Source
Thrown at hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java:579
property.setOptional( false );
}
};
// Always register this as a second pass and never execute it directly,
// even if we are in a second pass already. If we are in a second pass,
// then we are currently processing the generalSecondPassList
// to which the following call will add the second pass to,
// so it will be executed within that second pass, just a bit later
buildingContext.getMetadataCollector().addSecondPass( secondPass );
}
}
}
private void handleNaturalId(Property property) {
if ( memberDetails != null && entityBinder != null ) {
final var naturalId = memberDetails.getDirectAnnotationUsage( NaturalId.class );
if ( naturalId != null ) {
if ( !entityBinder.isRootEntity() ) {
throw new AnnotationException( "Property '" + qualify( holder.getPath(), name )
+ "' belongs to an entity subclass and may not be annotated '@NaturalId'" +
" (only a property of a root '@Entity' or a '@MappedSuperclass' may be a '@NaturalId')" );
}
if ( !naturalId.mutable() ) {
updatable = false;
}
property.setNaturalIdentifier( true );
}
}
}
private void inferOptimisticLocking(Property property) {
// this is already handled for collections in CollectionBinder...
if ( value instanceof org.hibernate.mapping.Collection collection ) {
property.setOptimisticLocked( collection.isOptimisticLocked() );
}
else if ( memberDetails != null && memberDetails.hasDirectAnnotationUsage( OptimisticLock.class ) ) {
final var optimisticLock = memberDetails.getDirectAnnotationUsage( OptimisticLock.class );View on GitHub (pinned to fad1729dce)
Solutions
- Move the @NaturalId property (and usually the field) up to the root @Entity of the hierarchy, or to a @MappedSuperclass if it is shared by several roots.
- If the key genuinely only applies to the subtype, drop @NaturalId and enforce uniqueness with a @Column(unique=true)/unique constraint plus a manual load-by-natural-key query instead.
- Reconsider the hierarchy direction: promote the subtype with the natural id to its own root entity.
Example fix
// before
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Person { ... }
@Entity
public class Employee extends Person {
@NaturalId
String ssn; // subclass property => rejected
}
// after: hoist to the root
public abstract class Person {
@NaturalId(mutable = true)
String ssn;
} Defensive patterns
Strategy: validation
Validate before calling
// Reject @NaturalId anywhere below the hierarchy root before boot
for (Class<?> entity : annotatedClasses) {
if (entity.getSuperclass() != null && entity.getSuperclass().isAnnotationPresent(Entity.class)) {
for (Field f : entity.getDeclaredFields()) {
if (f.isAnnotationPresent(NaturalId.class)) {
throw new IllegalStateException("@NaturalId on subclass property " + f + " of " + entity.getName());
}
}
}
} Type guard
static boolean isHierarchyRoot(Class<?> c) {
return c.getSuperclass() == null || !c.getSuperclass().isAnnotationPresent(Entity.class);
} Try / catch
try {
SessionFactory sf = cfg.buildSessionFactory();
} catch (AnnotationException e) {
throw new IllegalStateException("@NaturalId placement invalid: " + e.getMessage(), e);
} Prevention
- Keep natural-id fields on the root entity or a @MappedSuperclass
- Review natural-id placement whenever inheritance is introduced
When it happens
Trigger: Placing @NaturalId on a property of a JOINED, SINGLE_TABLE, or TABLE_PER_CLASS subclass entity; moving a natural-id field down from the root into a subclass during refactoring; annotating a subclass property in code generated from a template that assumed a root entity.
Common situations: Domain models where the business key only exists on a subtype (e.g. Employee.ssn under Person); refactoring hierarchies so a previously-root property becomes subclass-specific; onboarding legacy schemas with per-subtype natural keys.
Related errors
- Entity '" + propertyHolder.getEntityName() + "' is a subclas
- Table '${table}' has no column named '${column}' matching th
- Attribute '${attribute}' is declared as an '@Id' or '@Embedd
- Attribute '" + memberDetails.getName() + "' is declared by '
- proxy must be either an interface, or the class itself: {ent
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/aea091aafa4a4b1a.
Report an issue: GitHub.