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

  1. 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.
  2. 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.
  3. 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

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


AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22). Data as JSON: /api/errors/aea091aafa4a4b1a. Report an issue: GitHub.