hibernate/hibernate-orm · error · AnnotationException
Property '{}' is a {} and is directly annotated '@JoinColumn
Error message
Property '{}' is a {} and is directly annotated '@JoinColumn' (specify '@JoinColumn' inside '@JoinTable' or '@CollectionTable') What it means
For @ManyToMany and @ElementCollection, the foreign-key join column must be declared inside @JoinTable or @CollectionTable, not directly on the property. CollectionBinder.checkAnnotations throws this AnnotationException for a direct @JoinColumn/@JoinColumns placement because Hibernate cannot tell whether you mean the join table column or the element/target table column.
Source
Thrown at hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java:456
if ( oneToMany != null && property.hasDirectAnnotationUsage( SoftDelete.class ) ) {
throw new UnsupportedMappingException(
"@SoftDelete cannot be applied to @OneToMany - " +
property.getDeclaringType().getName() + "." + property.getName()
);
}
if ( property.hasDirectAnnotationUsage( OrderColumn.class )
&& manyToMany != null
&& isNotBlank( manyToMany.mappedBy() ) ) {
throw new AnnotationException("Collection '" + getPath( propertyHolder, inferredData ) +
"' is the unowned side of a bidirectional '@ManyToMany' and may not have an '@OrderColumn'");
}
if ( manyToMany != null || elementCollection != null ) {
if ( property.hasDirectAnnotationUsage( JoinColumn.class )
|| property.hasDirectAnnotationUsage( JoinColumns.class ) ) {
throw new AnnotationException( "Property '" + getPath( propertyHolder, inferredData )
+ "' is a " + annotationName( oneToMany, manyToMany, elementCollection )
+ " and is directly annotated '@JoinColumn'"
+ " (specify '@JoinColumn' inside '@JoinTable' or '@CollectionTable')" );
}
}
}
private static String annotationName(
OneToMany oneToMany,
ManyToMany manyToMany,
ElementCollection elementCollection) {
return oneToMany != null ? "'@OneToMany'" : manyToMany != null ? "'@ManyToMany'" : "'@ElementCollection'";
}
private static IndexColumn getIndexColumn(
PropertyHolder propertyHolder,
PropertyData inferredData,
EntityBinder entityBinder,View on GitHub (pinned to fad1729dce)
Solutions
- For @ManyToMany: move the column into @JoinTable(joinColumns = @JoinColumn(...), inverseJoinColumns = @JoinColumn(...))
- For @ElementCollection: move the column into @CollectionTable(joinColumns = @JoinColumn(...))
- Review each join column: joinColumns targets the owning table, inverseJoinColumns targets the target/element table
Example fix
// before
@Entity
class Employee {
@ManyToMany
@JoinColumn(name = "dept_id") // error: direct @JoinColumn on @ManyToMany
Set<Department> departments;
}
// after
@Entity
class Employee {
@ManyToMany
@JoinTable(name = "employee_department",
joinColumns = @JoinColumn(name = "employee_id"),
inverseJoinColumns = @JoinColumn(name = "department_id"))
Set<Department> departments;
} Defensive patterns
Strategy: validation
Validate before calling
// Reject direct @JoinColumn on @ManyToMany/@ElementCollection fields
static void checkJoinColumnPlacement(Class<?>... entities) {
for ( Class<?> c : entities ) {
for ( Field f : c.getDeclaredFields() ) {
boolean toManyOrElement = f.isAnnotationPresent( ManyToMany.class )
|| f.isAnnotationPresent( ElementCollection.class );
boolean directJoin = f.isAnnotationPresent( JoinColumn.class )
|| f.isAnnotationPresent( JoinColumns.class );
if ( toManyOrElement && directJoin ) {
throw new IllegalStateException( "@JoinColumn must sit inside @JoinTable/@CollectionTable: "
+ c.getName() + "." + f.getName() );
}
}
}
} Prevention
- Memorize the placement rule: @JoinColumn direct is for @ManyToOne/@OneToOne; @JoinTable/@CollectionTable wrap it for collection mappings
- Review generated mappings for @ManyToMany with a bare @JoinColumn - it is always invalid
- Add the reflection scan above to the mapping test suite
When it happens
Trigger: A property with @ManyToMany or @ElementCollection (manyToMany != null || elementCollection != null) also has a direct @JoinColumn or @JoinColumns annotation on the field/getter.
Common situations: Copying @JoinColumn patterns from @ManyToOne onto collection mappings; tools or generators emitting @JoinColumn next to @ManyToMany; misunderstanding which side of the @JoinTable the direct annotation would refer to.
Related errors
- Collection '<propertyName>' was annotated '@Collate'
- Property '${property}' defines a collection table '${collect
- A '@JoinColumn' references a column named '{}' but the targe
- Collection '{}' annotated '@NotFound' is not a '@ManyToMany'
- Property '{}' belongs to an '@Embeddable' class that is cont
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/b519751f1022c112.
Report an issue: GitHub.