{"record":{"id":"aea091aafa4a4b1a","repo":"hibernate/hibernate-orm","slug":"property-property-belongs-to-an-entity-subcla","errorCode":null,"errorMessage":"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')","messagePattern":"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'\\)","errorType":"exception","errorClass":"AnnotationException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java","lineNumber":579,"sourceCode":"\t\t\t\t\t\tproperty.setOptional( false );\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\t// Always register this as a second pass and never execute it directly,\n\t\t\t\t// even if we are in a second pass already. If we are in a second pass,\n\t\t\t\t// then we are currently processing the generalSecondPassList\n\t\t\t\t// to which the following call will add the second pass to,\n\t\t\t\t// so it will be executed within that second pass, just a bit later\n\t\t\t\tbuildingContext.getMetadataCollector().addSecondPass( secondPass );\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate void handleNaturalId(Property property) {\n\t\tif ( memberDetails != null && entityBinder != null ) {\n\t\t\tfinal var naturalId = memberDetails.getDirectAnnotationUsage( NaturalId.class );\n\t\t\tif ( naturalId != null ) {\n\t\t\t\tif ( !entityBinder.isRootEntity() ) {\n\t\t\t\t\tthrow new AnnotationException( \"Property '\" + qualify( holder.getPath(), name )\n\t\t\t\t\t\t\t+ \"' belongs to an entity subclass and may not be annotated '@NaturalId'\" +\n\t\t\t\t\t\t\t\" (only a property of a root '@Entity' or a '@MappedSuperclass' may be a '@NaturalId')\" );\n\t\t\t\t}\n\t\t\t\tif ( !naturalId.mutable() ) {\n\t\t\t\t\tupdatable = false;\n\t\t\t\t}\n\t\t\t\tproperty.setNaturalIdentifier( true );\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate void inferOptimisticLocking(Property property) {\n\t\t// this is already handled for collections in CollectionBinder...\n\t\tif ( value instanceof org.hibernate.mapping.Collection collection ) {\n\t\t\tproperty.setOptimisticLocked( collection.isOptimisticLocked() );\n\t\t}\n\t\telse if ( memberDetails != null && memberDetails.hasDirectAnnotationUsage( OptimisticLock.class ) ) {\n\t\t\tfinal var optimisticLock = memberDetails.getDirectAnnotationUsage( OptimisticLock.class );","sourceCodeStart":561,"sourceCodeEnd":597,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java#L561-L597","documentation":"@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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n@Inheritance(strategy = InheritanceType.JOINED)\npublic abstract class Person { ... }\n@Entity\npublic class Employee extends Person {\n    @NaturalId\n    String ssn;   // subclass property => rejected\n}\n\n// after: hoist to the root\npublic abstract class Person {\n    @NaturalId(mutable = true)\n    String ssn;\n}","handlingStrategy":"validation","validationCode":"// Reject @NaturalId anywhere below the hierarchy root before boot\nfor (Class<?> entity : annotatedClasses) {\n    if (entity.getSuperclass() != null && entity.getSuperclass().isAnnotationPresent(Entity.class)) {\n        for (Field f : entity.getDeclaredFields()) {\n            if (f.isAnnotationPresent(NaturalId.class)) {\n                throw new IllegalStateException(\"@NaturalId on subclass property \" + f + \" of \" + entity.getName());\n            }\n        }\n    }\n}","typeGuard":"static boolean isHierarchyRoot(Class<?> c) {\n    return c.getSuperclass() == null || !c.getSuperclass().isAnnotationPresent(Entity.class);\n}","tryCatchPattern":"try {\n    SessionFactory sf = cfg.buildSessionFactory();\n} catch (AnnotationException e) {\n    throw new IllegalStateException(\"@NaturalId placement invalid: \" + e.getMessage(), e);\n}","preventionTips":["Keep natural-id fields on the root entity or a @MappedSuperclass","Review natural-id placement whenever inheritance is introduced"],"tags":["hibernate","jpa","natural-id","inheritance","subclass","bootstrap"],"backgroundTag":"natural-id-misplaced","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}