{"record":{"id":"765b3bdb95bfce9d","repo":"hibernate/hibernate-orm","slug":"attributeconverter-class-s-registered-multiple-765b3b","errorCode":null,"errorMessage":"AttributeConverter class [%s] registered multiple times","messagePattern":"AttributeConverter class \\[(.+?)\\] registered multiple times","errorType":"exception","errorClass":"HibernateException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/boot/model/convert/internal/AttributeConverterManager.java","lineNumber":71,"sourceCode":"\t\tif ( registeredConversionsByDomainType != null ) {\n\t\t\tfinal var domainType = descriptor.getDomainValueResolvedType();\n\t\t\tfinal var registeredConversion = registeredConversionsByDomainType.get( domainType );\n\t\t\tif ( registeredConversion != null ) {\n\t\t\t\t// we can skip registering the converter, the RegisteredConversion will always take precedence\n\t\t\t\tif ( BOOT_LOGGER.isDebugEnabled() ) {\n\t\t\t\t\tBOOT_LOGGER.skippingRegistrationAttributeConverterForAutoApply( converterClass.getName() );\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tif ( attributeConverterDescriptorsByClass == null ) {\n\t\t\tattributeConverterDescriptorsByClass = new ConcurrentHashMap<>();\n\t\t}\n\n\t\tfinal Object old = attributeConverterDescriptorsByClass.put( converterClass, descriptor );\n\t\tif ( old != null ) {\n\t\t\tthrow new HibernateException(\n\t\t\t\t\tString.format(\n\t\t\t\t\t\t\tLocale.ENGLISH,\n\t\t\t\t\t\t\t\"AttributeConverter class [%s] registered multiple times\",\n\t\t\t\t\t\t\tconverterClass\n\t\t\t\t\t)\n\t\t\t);\n\t\t}\n\t}\n\n\tpublic void addRegistration(RegisteredConversion conversion) {\n\t\tif ( registeredConversionsByDomainType == null ) {\n\t\t\tregisteredConversionsByDomainType = new ConcurrentHashMap<>();\n\t\t}\n\n\t\tfinal var domainType = getDomainType( conversion );\n\t\tcheckNotOverriding( conversion, domainType );\n\t\t// See if we have a matching entry in attributeConverterDescriptorsByClass.\n\t\t// If so, remove it. The conversion being registered will always take precedence.","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/boot/model/convert/internal/AttributeConverterManager.java#L53-L89","documentation":"AttributeConverterManager keeps one descriptor per converter class in a ConcurrentHashMap; addAttributeConverter uses put() and throws HibernateException('AttributeConverter class [X] registered multiple times') whenever a previous entry existed. Any second registration of the same converter class — regardless of whether the descriptor is identical — is rejected.","triggerScenarios":"Calling MetadataBuilder.applyAttributeConverter(...) for a class that converter auto-discovery/scanning already registered, registering the same class in two configuration code paths (e.g., both a bootstrap hook and the main builder), or duplicated registration inside a loop over packages/entities.","commonSituations":"Framework integration code registering converters programmatically while @Converter-annotated classes are also scanned; refactors that moved registration into a shared method called twice; tests building several Metadata instances from a shared routine that accumulates registrations.","solutions":["Register each converter class from exactly one place: either rely on @Converter discovery or register programmatically, not both","Guard programmatic registration with a Set<Class<?>> of already-applied converters (see validation below)","If multiple builders are fed from one config routine, make it idempotent or re-create it per builder"],"exampleFix":"// before\nmetadataBuilder.applyAttributeConverter(MoneyConverter.class);\n// ... later, another module also does:\nmetadataBuilder.applyAttributeConverter(MoneyConverter.class); // throws\n\n// after\nprivate static final Set<Class<? extends AttributeConverter<?,?>>> REGISTERED = ConcurrentHashMap.newKeySet();\nif (REGISTERED.add(MoneyConverter.class)) {\n    metadataBuilder.applyAttributeConverter(MoneyConverter.class);\n}","handlingStrategy":"validation","validationCode":"private final Set<Class<? extends AttributeConverter<?,?>>> registered = java.util.concurrent.ConcurrentHashMap.newKeySet();\n\nvoid registerOnce(MetadataBuilder builder, Class<? extends AttributeConverter<?,?>> clazz) {\n    if (registered.add(clazz)) {\n        builder.applyAttributeConverter(clazz);\n    }\n    // silently skip: class already registered via scan or earlier call\n}","typeGuard":null,"tryCatchPattern":"catch (HibernateException e) {\n    if (String.valueOf(e.getMessage()).contains(\"registered multiple times\")) {\n        throw new IllegalStateException(\"Converter registered twice - deduplicate applyAttributeConverter/scanning paths\", e);\n    }\n    throw e;\n}","preventionTips":["Choose one registration mechanism: annotations with scanning, or programmatic - not both","Centralize converter registration in a single bootstrap component","Make shared bootstrap config idempotent when reused across builders"],"tags":["jpa","attribute-converter","bootstrap","duplicate-registration"],"backgroundTag":"duplicate-converter-registration","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}