{"record":{"id":"2932a2ed5de89d9e","repo":"hibernate/hibernate-orm","slug":"assigned-tenant-id-differs-from-current-tenant-id","errorCode":null,"errorMessage":"assigned tenant id differs from current tenant id [{} != {}]","messagePattern":"assigned tenant id differs from current tenant id \\[(.+?) != (.+?)\\]","errorType":"exception","errorClass":"PropertyValueException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/generator/internal/TenantIdGeneration.java","lineNumber":68,"sourceCode":"\t@Override\n\tpublic Class<?> getGeneratedType() {\n\t\treturn generatedType;\n\t}\n\n\t@Override\n\tpublic Object generate(SharedSessionContractImplementor session, Object owner, Object currentValue, EventType eventType) {\n\t\tfinal var sessionFactory = session.getSessionFactory();\n\t\tfinal Object tenantId = session.getTenantIdentifierValue();\n\t\tif ( currentValue != null ) {\n\t\t\tfinal var resolver = sessionFactory.getCurrentTenantIdentifierResolver();\n\t\t\tif ( resolver != null && resolver.isRoot( tenantId ) ) {\n\t\t\t\t// the \"root\" tenant is allowed to set the tenant id explicitly\n\t\t\t\treturn currentValue;\n\t\t\t}\n\t\t\telse {\n\t\t\t\tfinal var tenantIdJavaType = sessionFactory.getTenantIdentifierJavaType();\n\t\t\t\tif ( !tenantIdJavaType.areEqual( currentValue, tenantId ) ) {\n\t\t\t\t\tthrow new PropertyValueException(\n\t\t\t\t\t\t\t\"assigned tenant id differs from current tenant id [\"\n\t\t\t\t\t\t\t\t\t+ tenantIdJavaType.toString( currentValue )\n\t\t\t\t\t\t\t\t\t+ \" != \"\n\t\t\t\t\t\t\t\t\t+ tenantIdJavaType.toString( tenantId ) + \"]\",\n\t\t\t\t\t\t\tentityName,\n\t\t\t\t\t\t\tpropertyName\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn tenantId;\n\t}\n}\n","sourceCodeStart":50,"sourceCodeEnd":82,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/generator/internal/TenantIdGeneration.java#L50-L82","documentation":"@TenantId assigns the current session tenant identifier to a tenant-discriminator attribute on insert and update. If the entity already carries a tenant id that differs from the session's current tenant — and the CurrentTenantIdentifierResolver does not report the current tenant as a 'root' tenant allowed to write foreign tenants — flush fails with this PropertyValueException. It is a data-integrity guard against cross-tenant writes.","triggerScenarios":"Flushing an entity whose @TenantId field holds a value != session.getTenantIdentifierValue(): loading under tenant A and merging/updating in a session bound to tenant B; pre-populated or deserialized entities with a stale tenant field; a resolver whose isRoot(...) returns false for an admin tenant that legitimately writes other tenants' rows.","commonSituations":"Multi-tenant request processing where the tenant context (ThreadLocal/request scope) changed between load and save; background jobs replaying detached entities under a system tenant; misconfigured CurrentTenantIdentifierResolver.isRoot(); tenant identifier Java type mismatches (String vs UUID) so areEqual never matches.","solutions":["Ensure the entity's tenant field matches the session tenant before flushing — reload or merge within the same tenant's session","If an admin/root tenant must write other tenants' rows, implement CurrentTenantIdentifierResolver.isRoot(currentTenant) to return true for it","Stop reusing detached instances across tenant contexts; keep entity instances per tenant","If the mismatch is a false positive, align the tenant identifier Java type (e.g. String vs UUID) across resolver, SessionFactory, and entity field so areEqual compares consistently","Clear or refresh the stale tenant field before merging"],"exampleFix":"// before — order loaded under tenant \"acme\", flushed in a session bound to \"globex\"\nsessionGlobex.merge(order);   // PropertyValueException: assigned tenant id differs\n\n// after — verify tenant ownership before writing\nif (!Objects.equals(order.getTenant(), sessionGlobex.getTenantIdentifierValue())) {\n    throw new SecurityException(\"cross-tenant write blocked\");\n}\nsessionGlobex.merge(order);","handlingStrategy":"validation","validationCode":"// Call before persist/merge of entities carrying @TenantId\nstatic void assertSameTenant(Object entityTenantValue, SharedSessionContract session) {\n    Object current = session.getTenantIdentifierValue();\n    if (entityTenantValue != null && !entityTenantValue.equals(current)) {\n        throw new IllegalStateException(\"entity tenant [\" + entityTenantValue\n                + \"] does not match session tenant [\" + current + \"]\");\n    }\n}","typeGuard":null,"tryCatchPattern":"try {\n    session.merge(order);\n    session.flush();\n}\ncatch (PropertyValueException e) {\n    if (e.getMessage() != null && e.getMessage().startsWith(\"assigned tenant id differs\")) {\n        // cross-tenant write attempt: abort and audit, never blind-retry\n        throw new TenantAccessException(order.getTenant(), session.getTenantIdentifierValue(), e);\n    }\n    throw e;\n}","preventionTips":["Open and use one session per tenant request; never share sessions across tenant switches","Implement CurrentTenantIdentifierResolver.isRoot() for legitimate super-tenant/admin writers","Add integration tests that switch tenants mid-conversation","Keep the tenant id Java type consistent across resolver, SessionFactory, and entity field"],"tags":["hibernate","multi-tenancy","tenant-id","flush","data-integrity"],"backgroundTag":"tenant-id-mismatch","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}