jwtk/jjwt · error · io.jsonwebtoken.security.MalformedKeyException
JWK kty value must be a String. Type found: ${type}
Error message
JWK kty value must be a String. Type found: ${type} What it means
Thrown by JwkConverter.applyFrom when the kty (key type) member of a JWK JSON object is present but is not a JSON string. RFC 7517 §4.1 requires kty to be a case-sensitive string (e.g. "oct", "RSA", "EC", "OKP"), so a non-String value marks the JWK as malformed and conversion fails with MalformedKeyException.
Source
Thrown at impl/src/main/java/io/jsonwebtoken/impl/security/JwkConverter.java:152
throw new IllegalArgumentException(msg);
}
final Map<?, ?> map = Collections.immutable((Map<?, ?>) o);
Parameter<String> param = AbstractJwk.KTY;
// mandatory for all JWKs: https://datatracker.ietf.org/doc/html/rfc7517#section-4.1
// no need for builder param type conversion overhead if this isn't present:
if (Collections.isEmpty(map) || !map.containsKey(param.getId())) {
String msg = "JWK is missing required " + param + " parameter.";
throw new MalformedKeyException(msg);
}
Object val = map.get(param.getId());
if (val == null) {
String msg = "JWK " + param + " value cannot be null.";
throw new MalformedKeyException(msg);
}
if (!(val instanceof String)) {
String msg = "JWK " + param + " value must be a String. Type found: " + val.getClass().getName();
throw new MalformedKeyException(msg);
}
String kty = (String) val;
if (!Strings.hasText(kty)) {
String msg = "JWK " + param + " value cannot be empty.";
throw new MalformedKeyException(msg);
}
DynamicJwkBuilder<?, ?> builder = this.supplier.get();
for (Map.Entry<?, ?> entry : map.entrySet()) {
Object key = entry.getKey();
Assert.notNull(key, "JWK map key cannot be null.");
if (!(key instanceof String)) {
String msg = "JWK map keys must be Strings. Encountered key '" + key + "' of type " +
key.getClass().getName() + ".";
throw new IllegalArgumentException(msg);
}
String skey = (String) key;
builder.add(skey, entry.getValue());View on GitHub (pinned to fb71496164)
Solutions
- Set kty as a plain String ("RSA", "EC", "oct", "OKP")
- If using an enum, pass enum.name() or toString() instead of the enum instance
- Fix custom deserializers that map kty to non-String types
Example fix
// before
Map<String,Object> jwk = Map.of("kty", KeyType.RSA, "n", n, "e", e);
// after
Map<String,Object> jwk = Map.of("kty", KeyType.RSA.name(), "n", n, "e", e); Defensive patterns
Strategy: type-guard
Validate before calling
if (!(jwkMap.get("kty") instanceof String)) { throw new IllegalArgumentException("kty must be a String"); } Type guard
String ktyOrNull(Map<String,?> m) { Object v = m == null ? null : m.get("kty"); return v instanceof String s ? s : null; } Try / catch
try { jwk = Jwks.builder().build(); } catch (MalformedKeyException e) { log.error("kty must be a String: {}", e.getMessage()); } Prevention
- Use enum.name() when mapping key-type enums to JWK fields
- Keep JWK maps strictly Map<String,Object> with JSON-compatible types
When it happens
Trigger: Passing a map where kty is a non-String object (e.g. a Map, List, enum object, or number) into JWK conversion.
Common situations: Programmatically constructed maps where kty was set to a custom key-type enum instead of its String value; custom deserializers producing non-String values.
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
- JWK map keys must be Strings. Encountered key '${key}' of ty
- ${message}Object of class [${objClassName}] must be an insta
- ${message}${subType} is not assignable to ${superType}
- JWT Claim '<expectedClaimName>' was expected to be a Date, b
- JWK must be a Map<String,?> (JSON Object). Type found: ${typ
AI-assisted analysis of jwtk/jjwt@fb71496164 (2026-09-09).
Data as JSON: /api/errors/b4e0a8d646652961.
Report an issue: GitHub.