{"record":{"id":"be9d830478b6d441","repo":"quarkusio/quarkus","slug":"messagebundle-must-be-declared-on-an-interface","errorCode":null,"errorMessage":"@MessageBundle must be declared on an interface: {bundleClass}","messagePattern":"@MessageBundle must be declared on an interface: (.+?)","errorType":"exception","errorClass":"MessageBundleException","httpStatus":null,"severity":"error","filePath":"extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java","lineNumber":253,"sourceCode":"                            } else {\n                                files = localeToFiles.get(locale);\n                                if (files == null) {\n                                    files = new ArrayList<>();\n                                    localeToFiles.put(locale, files);\n                                }\n                            }\n                            files.add(messageFile);\n                        }\n                    }\n\n                    // Check for duplicates again\n                    checkForDuplicates(localeToMergeCandidates);\n                    checkForDuplicates(localeToFiles);\n\n                    bundles.add(new MessageBundleBuildItem(name, bundleClass, localeToInterface,\n                            localeToFiles, localeToMergeCandidates, defaultLocale));\n                } else {\n                    throw new MessageBundleException(\"@MessageBundle must be declared on an interface: \" + bundleClass);\n                }\n            }\n        }\n\n        // Detect interfaces annotated with @Localized that don't extend a message bundle interface\n        for (AnnotationInstance localizedAnnotation : index.getAnnotations(Names.LOCALIZED)) {\n            if (localizedAnnotation.target().kind() == Kind.CLASS) {\n                ClassInfo localized = localizedAnnotation.target().asClass();\n                if (Modifier.isInterface(localized.flags())) {\n                    if (!localizedInterfaces.contains(localized.name())) {\n                        throw new MessageBundleException(\n                                \"A localized message bundle interface must extend a message bundle interface: \" + localized);\n                    }\n                } else {\n                    throw new MessageBundleException(\"@Localized must be declared on an interface: \" + localized);\n                }\n            }\n        }","sourceCodeStart":235,"sourceCodeEnd":271,"githubUrl":"https://github.com/quarkusio/quarkus/blob/e1c734241f34c7919086ceb4c9262b4a58f6de44/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java#L235-L271","documentation":"Quarkus Qute message bundles are defined as Java interfaces annotated with @MessageBundle. During application build, MessageBundleProcessor.processBundles validates that every class carrying @MessageBundle is actually an interface; if it is a class, enum, or record, the build fails with this MessageBundleException. This is a compile/deploy-time contract check so template resolution can generate a implementing class.","triggerScenarios":"Annotating a concrete class, abstract class, enum, or record with @MessageBundle instead of an interface, e.g. `@MessageBundle(locale=\"en\") public class AppMessages {...}`.","commonSituations":"Copy-pasting the annotation from an interface onto a class; migrating a messages class from another i18n framework (e.g. ResourceBundle-based) without converting it to an interface; auto-generated code that emits classes.","solutions":["Change the annotated type to a Java interface: `@MessageBundle(locale = \"en\") public interface AppMessages { ... }`","If a class is required, move the bundle methods into a new interface and have the class consume the generated bundle bean instead.","Rebuild and confirm the annotation target is reported as an interface in the error location."],"exampleFix":"// before\n@MessageBundle(locale = \"en\")\npublic class AppMessages {\n    String hello(String name);\n}\n\n// after\n@MessageBundle(locale = \"en\")\npublic interface AppMessages {\n    String hello(String name);\n}","handlingStrategy":"validation","validationCode":"// Verify every @MessageBundle target is an interface before building\nClass<?> c = AppMessages.class;\nif (!c.isInterface() || c.getAnnotation(MessageBundle.class) == null) {\n    throw new IllegalStateException(\"@MessageBundle must be on an interface: \" + c);\n}","typeGuard":"static boolean isValidMessageBundle(Class<?> c) {\n    return c.isInterface() && c.isAnnotationPresent(MessageBundle.class);\n}","tryCatchPattern":null,"preventionTips":["Always define bundles as interfaces, never classes","Let annotation-processor errors surface early in dev mode instead of ignoring build output","Keep bundle interfaces in a dedicated i18n package to avoid accidental class conversion"],"tags":["quarkus","qute","message-bundle","build-time"],"backgroundTag":"message-bundle-annotation-misuse","analyzedSha":"e1c734241f34c7919086ceb4c9262b4a58f6de44","analyzedAt":"2026-09-05T17:01:29.979Z","contentChangedAt":"2026-09-05T17:01:29.979Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}