hibernate/hibernate-orm · error · AnnotationException
Property '${property}' is annotated '@${annotation}' but is
Error message
Property '${property}' is annotated '@${annotation}' but is not of type '${type}' What it means
PropertyBinder.checkAnnotation enforces that collection-oriented JPA annotations are only placed on attributes of the matching container type: @OrderColumn requires a List, and the @MapKey family (@MapKey, @MapKeyColumn, @MapKeyClass, @MapKeyEnumerated, @MapKeyTemporal, @MapKeyJoinColumn(s)) requires a Map. Violations throw AnnotationException at bootstrap with the offending annotation and required type in the message.
Source
Thrown at hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java:671
throw new AnnotationException( "Property '" + qualify( holder.getPath(), name )
+ "' is annotated '@OrderBy' but is not of type 'Collection' or 'Map'" );
}
}
checkAnnotation( MapKey.class, Map.class );
checkAnnotation( MapKeyColumn.class, Map.class );
checkAnnotation( MapKeyClass.class, Map.class );
checkAnnotation( MapKeyEnumerated.class, Map.class );
checkAnnotation( MapKeyTemporal.class, Map.class );
checkAnnotation( MapKeyColumn.class, Map.class );
checkAnnotation( MapKeyJoinColumn.class, Map.class );
checkAnnotation( MapKeyJoinColumns.class, Map.class );
}
}
private void checkAnnotation(Class<? extends Annotation> annotationClass, Class<?> propertyType) {
if ( memberDetails.hasDirectAnnotationUsage( annotationClass )
&& !memberDetails.getType().isImplementor( propertyType ) ) {
throw new AnnotationException( "Property '" + qualify( holder.getPath(), name )
+ "' is annotated '@" + annotationClass.getSimpleName()
+ "' but is not of type '" + propertyType.getTypeName() + "'" );
}
}
private void validateOptimisticLock(boolean excluded) {
if ( excluded ) {
if ( isVersion( memberDetails ) ) {
throw new AnnotationException("Property '" + qualify( holder.getPath(), name )
+ "' is annotated '@OptimisticLock(excluded=true)' and '@Version'" );
}
if ( isSimpleId( memberDetails ) ) {
throw new AnnotationException("Property '" + qualify( holder.getPath(), name )
+ "' is annotated '@OptimisticLock(excluded=true)' and '@Id'" );
}
if ( isEmbeddedId( memberDetails ) ) {
throw new AnnotationException( "Property '" + qualify( holder.getPath(), name )
+ "' is annotated '@OptimisticLock(excluded=true)' and '@EmbeddedId'" );View on GitHub (pinned to fad1729dce)
Solutions
- Match annotation to container: keep @OrderColumn only on List, keep @MapKey* only on Map.
- If the field became a List, replace the @MapKey* annotations with @MapKeyColumn-style equivalents only where valid, or drop them.
- If the field became a Map, remove @OrderColumn (maps are keyed, not indexed).
- Change the field type back if the annotations reflect the real intent (e.g. it should always have been a SortedMap).
Example fix
// before @OrderColumn(name = "position") private Set<Line> lines; // Set has no index // after (choose one) @OrderColumn(name = "position") private List<Line> lines = new ArrayList<>(); // or, if it must stay a Set: // remove @OrderColumn and sort with @OrderBy / SortedSet semantics
Defensive patterns
Strategy: validation
Validate before calling
// Enforce annotation/container pairing before boot
Map<Class<? extends Annotation>, Class<?>> rules = Map.of(
OrderColumn.class, List.class,
MapKey.class, Map.class,
MapKeyColumn.class, Map.class,
MapKeyClass.class, Map.class,
MapKeyEnumerated.class, Map.class,
MapKeyTemporal.class, Map.class,
MapKeyJoinColumn.class, Map.class,
MapKeyJoinColumns.class, Map.class);
for (Class<?> entity : annotatedClasses) {
for (Field f : entity.getDeclaredFields()) {
rules.forEach((ann, required) -> {
if (f.isAnnotationPresent(ann) && !required.isAssignableFrom(f.getType())) {
throw new IllegalStateException(f + " has @" + ann.getSimpleName() + " but is not a " + required.getSimpleName());
}
});
}
} Type guard
static boolean annotationMatchesType(Field f, Class<? extends Annotation> ann, Class<?> required) {
return !f.isAnnotationPresent(ann) || required.isAssignableFrom(f.getType());
} Try / catch
try {
SessionFactory sf = cfg.buildSessionFactory();
} catch (AnnotationException e) {
throw new IllegalStateException("Annotation/type mismatch: " + e.getMessage(), e);
} Prevention
- @OrderColumn only on List; @MapKey* only on Map
- Sweep annotations whenever a field's container type changes
- Consider an ArchUnit or custom reflection rule to enforce pairing in CI
When it happens
Trigger: @OrderColumn on a Set or Collection field (only List keeps an index); @MapKey on a List or Collection field; @MapKeyEnumerated/@MapKeyColumn on a non-Map association; changing a Map field to List (or vice versa) during refactoring while keeping the key/order annotations.
Common situations: Switching container types (Map to List, Set to List) when the domain model changes; copy-pasting annotations between fields of different types; generated metamodel or scaffolding code applying Map annotations to generic collections.
Related errors
- Property '${property}' is annotated '@OrderBy' but is not of
- Member '" + memberDetails.getName() + "' of embeddable class
- AttributeConverter class [%s] registered multiple times
- Duplicate named query '%s'
- Duplicate named stored procedure '{}'
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/a572f634b8c9134d.
Report an issue: GitHub.