{"record":{"id":"b0af063f040d95f4","repo":"oracle/graal","slug":"annotation-value-type-s-is-not-assignable-to-s","errorCode":null,"errorMessage":"Annotation value type %s is not assignable to %s","messagePattern":"Annotation value type (.+?) is not assignable to (.+?)","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"compiler/src/jdk.graal.compiler.vmaccess/src/jdk/graal/compiler/vmaccess/HostAnnotationValueConverter.java","lineNumber":98,"sourceCode":"     */\n    public static <T extends Annotation> T toAnnotation(AnnotationValue annotationValue, Class<T> expectedType, Function<ResolvedJavaType, Class<?>> typeToClass) {\n        if (annotationValue == null) {\n            return null;\n        }\n        Objects.requireNonNull(expectedType);\n        Objects.requireNonNull(typeToClass);\n        if (annotationValue.isError()) {\n            throw annotationValue.getError();\n        }\n        Class<?> actualType = typeToClass.apply(annotationValue.getAnnotationType());\n        if (actualType == null) {\n            throw new IllegalArgumentException(\"Annotation value type \" + annotationValue.getAnnotationType().toJavaName() + \" has no host class\");\n        }\n        if (!actualType.isAnnotation()) {\n            throw new IllegalArgumentException(\"Annotation value type \" + actualType.getName() + \" is not an annotation interface\");\n        }\n        if (!expectedType.isAssignableFrom(actualType)) {\n            throw new IllegalArgumentException(\"Annotation value type \" + actualType.getName() + \" is not assignable to \" + expectedType.getName());\n        }\n        Class<? extends Annotation> annotationType = actualType.asSubclass(Annotation.class);\n        Annotation annotation = annotationValue.toAnnotation(annotationType, (value, type) -> createAnnotation(value, type, typeToClass));\n        return expectedType.cast(annotation);\n    }\n\n    /**\n     * Materializes a host-owned JDK annotation proxy from JVMCI annotation metadata.\n     */\n    private static <T extends Annotation> T createAnnotation(AnnotationValue annotationValue, Class<T> annotationType, Function<ResolvedJavaType, Class<?>> typeToClass) {\n        Map<String, Object> memberValues = new LinkedHashMap<>();\n        Map<String, Object> annotationElements = annotationValue.getElements();\n        AnnotationValueType annotationValueType = AnnotationValueType.getInstance(annotationValue.getAnnotationType());\n        AnnotationValueValidation.validateElements(annotationValue, annotationValueType);\n        Map<String, Object> memberDefaults = annotationValueType.memberDefaults();\n        for (Method member : annotationType.getDeclaredMethods()) {\n            String memberName = member.getName();\n            Object memberValue = annotationElements.get(memberName);","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/oracle/graal/blob/a66e9ccd1d7bf2552883939aa0788dfd0e294aab/compiler/src/jdk.graal.compiler.vmaccess/src/jdk/graal/compiler/vmaccess/HostAnnotationValueConverter.java#L80-L116","documentation":"The third structural check in HostAnnotationValueConverter.toAnnotation: the resolved annotation class must be assignable to the caller's expectedType parameter, otherwise this exception names both classes. It exists so a mismatched cast fails immediately with context instead of ClassCastException deep inside the JDK proxy creation.","triggerScenarios":"Calling toAnnotation(annotationValue, MyAnnotation.class, ...) when the AnnotationValue's actual type is a different annotation (e.g. passing the wrong expectedType constant, or receiving an annotation you did not ask for).","commonSituations":"Generic code that assumes all annotations in a set share a type; refactorings that change the queried annotation class but not all call sites; processing mixed annotation metadata where a member annotation has a different type than the outer one.","solutions":["Pass the expectedType that matches the annotation actually stored in the AnnotationValue (check getAnnotationType().toJavaName()).","Branch on the resolved annotation type before conversion when handling heterogeneous annotations.","If a member annotation can legitimately be several types, dispatch on getAnnotationType() rather than forcing one expectedType.","Update stale expectedType constants after refactors."],"exampleFix":"// before\nHostAnnotationValueConverter.toAnnotation(av, CacheKey.class, fn); // av is really @Cached\n// after\nClass<? extends Annotation> t = switch (av.getAnnotationType().toJavaName()) {\n    case \"jdk.vm.ci.meta.Cached\" -> Cached.class;\n    default -> throw new IllegalStateException(\"unexpected \" + av.getAnnotationType());\n};\nHostAnnotationValueConverter.toAnnotation(av, t, fn);","handlingStrategy":"validation","validationCode":"String actual = annotationValue.getAnnotationType().toJavaName();\nif (!expectedType.getName().equals(actual) && !expectedType.getName().startsWith(actual)) {\n    // pick the right expectedType for `actual` before calling toAnnotation\n}","typeGuard":"static boolean annotationTypeIs(AnnotationValue av, Class<? extends Annotation> expected) {\n    return av.getAnnotationType().toJavaName().equals(expected.getName());\n}","tryCatchPattern":"try { toAnnotation(av, Expected.class, fn); } catch (IllegalArgumentException e) { if (e.getMessage().contains(\"not assignable to\")) { /* dispatch on av.getAnnotationType() instead */ } else throw e; }","preventionTips":["Dispatch on getAnnotationType().toJavaName() for heterogeneous annotation sets.","Update expectedType constants after refactors.","Do not assume all member annotations share the outer annotation's type."],"tags":["graalvm","jvmci","annotations","type-mismatch"],"backgroundTag":null,"analyzedSha":"a66e9ccd1d7bf2552883939aa0788dfd0e294aab","analyzedAt":"2026-08-14T13:58:47.161Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}