{"record":{"id":"b519751f1022c112","repo":"hibernate/hibernate-orm","slug":"property-is-a-and-is-directly-annotated","errorCode":null,"errorMessage":"Property '{}' is a {} and is directly annotated '@JoinColumn' (specify '@JoinColumn' inside '@JoinTable' or '@CollectionTable')","messagePattern":"Property '(.+?)' is a (.+?) and is directly annotated '@JoinColumn' \\(specify '@JoinColumn' inside '@JoinTable' or '@CollectionTable'\\)","errorType":"exception","errorClass":"AnnotationException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java","lineNumber":456,"sourceCode":"\n\t\tif ( oneToMany != null && property.hasDirectAnnotationUsage( SoftDelete.class ) ) {\n\t\t\tthrow new UnsupportedMappingException(\n\t\t\t\t\t\"@SoftDelete cannot be applied to @OneToMany - \" +\n\t\t\t\t\t\t\tproperty.getDeclaringType().getName() + \".\" + property.getName()\n\t\t\t);\n\t\t}\n\n\t\tif ( property.hasDirectAnnotationUsage( OrderColumn.class )\n\t\t\t\t&& manyToMany != null\n\t\t\t\t&& isNotBlank( manyToMany.mappedBy() ) ) {\n\t\t\tthrow new AnnotationException(\"Collection '\" + getPath( propertyHolder, inferredData ) +\n\t\t\t\t\t\"' is the unowned side of a bidirectional '@ManyToMany' and may not have an '@OrderColumn'\");\n\t\t}\n\n\t\tif ( manyToMany != null || elementCollection != null ) {\n\t\t\tif ( property.hasDirectAnnotationUsage( JoinColumn.class )\n\t\t\t\t\t|| property.hasDirectAnnotationUsage( JoinColumns.class ) ) {\n\t\t\t\tthrow new AnnotationException( \"Property '\" + getPath( propertyHolder, inferredData )\n\t\t\t\t\t\t+ \"' is a \" + annotationName( oneToMany, manyToMany, elementCollection )\n\t\t\t\t\t\t+ \" and is directly annotated '@JoinColumn'\"\n\t\t\t\t\t\t+ \" (specify '@JoinColumn' inside '@JoinTable' or '@CollectionTable')\" );\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate static String annotationName(\n\t\t\tOneToMany oneToMany,\n\t\t\tManyToMany manyToMany,\n\t\t\tElementCollection elementCollection) {\n\t\treturn oneToMany != null ? \"'@OneToMany'\" : manyToMany != null ? \"'@ManyToMany'\" : \"'@ElementCollection'\";\n\t}\n\n\tprivate static IndexColumn getIndexColumn(\n\t\t\tPropertyHolder propertyHolder,\n\t\t\tPropertyData inferredData,\n\t\t\tEntityBinder entityBinder,","sourceCodeStart":438,"sourceCodeEnd":474,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java#L438-L474","documentation":"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.","triggerScenarios":"A property with @ManyToMany or @ElementCollection (manyToMany != null || elementCollection != null) also has a direct @JoinColumn or @JoinColumns annotation on the field/getter.","commonSituations":"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.","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"],"exampleFix":"// before\n@Entity\nclass Employee {\n    @ManyToMany\n    @JoinColumn(name = \"dept_id\")   // error: direct @JoinColumn on @ManyToMany\n    Set<Department> departments;\n}\n\n// after\n@Entity\nclass Employee {\n    @ManyToMany\n    @JoinTable(name = \"employee_department\",\n        joinColumns = @JoinColumn(name = \"employee_id\"),\n        inverseJoinColumns = @JoinColumn(name = \"department_id\"))\n    Set<Department> departments;\n}","handlingStrategy":"validation","validationCode":"// Reject direct @JoinColumn on @ManyToMany/@ElementCollection fields\nstatic void checkJoinColumnPlacement(Class<?>... entities) {\n    for ( Class<?> c : entities ) {\n        for ( Field f : c.getDeclaredFields() ) {\n            boolean toManyOrElement = f.isAnnotationPresent( ManyToMany.class )\n                || f.isAnnotationPresent( ElementCollection.class );\n            boolean directJoin = f.isAnnotationPresent( JoinColumn.class )\n                || f.isAnnotationPresent( JoinColumns.class );\n            if ( toManyOrElement && directJoin ) {\n                throw new IllegalStateException( \"@JoinColumn must sit inside @JoinTable/@CollectionTable: \"\n                    + c.getName() + \".\" + f.getName() );\n            }\n        }\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["hibernate","jpa","join-column","join-table","collection-table","many-to-many","element-collection"],"backgroundTag":"joincolumn-placement-error","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}