spring-projects/spring-security · error · ClassCastException
principal + " is not assignable to " + parameter.getParamete
Error message
principal + " is not assignable to " + parameter.getParameterType()
What it means
The current org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver resolves @AuthenticationPrincipal parameters, optionally applying a SpEL expression to the principal. If the resulting principal (raw or expression-evaluated) is not assignable to the declared parameter type and the annotation's errorOnInvalidType is true, resolveArgument throws a ClassCastException naming the principal and the target parameter type.
Source
Thrown at web/src/main/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.java:135
Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
if (authentication == null) {
return null;
}
Object principal = authentication.getPrincipal();
AuthenticationPrincipal annotation = findMethodAnnotation(parameter);
Assert.notNull(annotation, "@AuthenticationPrincipal is required. Call supportsParameter first.");
String expressionToParse = annotation.expression();
if (StringUtils.hasLength(expressionToParse)) {
StandardEvaluationContext context = new StandardEvaluationContext();
context.setRootObject(principal);
context.setVariable("this", principal);
context.setBeanResolver(this.beanResolver);
Expression expression = this.parser.parseExpression(expressionToParse);
principal = expression.getValue(context);
}
if (principal != null && !ClassUtils.isAssignable(parameter.getParameterType(), principal.getClass())) {
if (annotation.errorOnInvalidType()) {
throw new ClassCastException(principal + " is not assignable to " + parameter.getParameterType());
}
return null;
}
return principal;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return findMethodAnnotation(parameter) != null;
}
/**
* Sets the {@link BeanResolver} to be used on the expressions.
* @param beanResolver the {@link BeanResolver} to use
*/
public void setBeanResolver(BeanResolver beanResolver) {
this.beanResolver = beanResolver;
}View on GitHub (pinned to 96852e8860)
Solutions
- Verify the expression's evaluated type matches the parameter type (e.g. principal.claims['sub'] returns String vs Long) and correct the expression or parameter declaration.
- Remove errorOnInvalidType=true so a mismatch yields null instead of a 500 ClassCastException, and handle null explicitly.
- Unify principal types across all authentication mechanisms (custom UserDetails adopted by OAuth2/JWT login too).
- Add a test that authenticates through the real mechanism and invokes the controller to catch type drift early.
Example fix
// before
public OrderDto get(@AuthenticationPrincipal(errorOnInvalidType = true, expression = "attributes['sub']") MyUser user)
// after (expression returns a String, so accept String)
public OrderDto get(@AuthenticationPrincipal(expression = "attributes['sub']") String userId) { ... } Defensive patterns
Strategy: type-guard
Validate before calling
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Object principal = auth == null ? null : auth.getPrincipal();
if (principal != null && !ClassUtils.isAssignable(MyUser.class, principal.getClass())) {
throw new AccessDeniedException("Principal " + principal.getClass() + " is not MyUser");
}
Type guard
static <T> T principalAs(Class<T> type) {
Authentication a = SecurityContextHolder.getContext().getAuthentication();
Object p = a == null ? null : a.getPrincipal();
return type.isInstance(p) ? type.cast(p) : null;
} Try / catch
try {
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
} catch (ClassCastException e) {
log.warn("@AuthenticationPrincipal type mismatch (check expression result type): {}", e.getMessage());
return null; // or map to 403 depending on policy
} Prevention
- Check the SpEL expression's return type matches the declared parameter type
- Migrate carefully when switching to JWT/OAuth2 — principal type changes from UserDetails to String/Jwt
- Leave errorOnInvalidType at its default (false) unless a hard failure is required
- Write controller tests that authenticate via the real security config to catch type drift
When it happens
Trigger: A controller declares @AuthenticationPrincipal(expression = "...") CustomType param (or plain @AuthenticationPrincipal CustomType) with errorOnInvalidType=true, but the SecurityContext principal — or the SpEL expression's result — is of an unrelated runtime type, so ClassUtils.isAssignable fails.
Common situations: SpEL expressions returning the wrong property type (e.g. expression yielding a String while the parameter expects a Long or custom type); principal type changed after migrating from session login to JWT/OAuth2; method-security or tests injecting a different Authentication; copy-pasted controllers across apps with different UserDetails classes.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- principal + " is not assignable to " + parameter.getParamete
- securityContextResult + " is not assignable to " + parameter
- <principal> is not assignable to <parameterType>
- <securityContextResult> is not assignable to <parameterType>
- Access is denied
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/db520503f642ff15.
Report an issue: GitHub.