jwtk/jjwt · error · io.jsonwebtoken.security.MalformedKeySetException
JWK Set keys value must be a Collection (JSON Array). Type f
Error message
JWK Set keys value must be a Collection (JSON Array). Type found: ${type} What it means
Thrown by JwkSetConverter.applyFrom when the keys member of a JWK Set is present and non-null but is not a JSON Array (Collection). RFC 7517 §5 requires keys to be an array of JWK objects, so any other JSON type marks the set malformed and conversion fails with MalformedKeySetException.
Source
Thrown at impl/src/main/java/io/jsonwebtoken/impl/security/JwkSetConverter.java:97
throw new IllegalArgumentException(msg);
}
final Map<?, ?> m = Collections.immutable((Map<?, ?>) o);
// mandatory for all JWK Sets: https://datatracker.ietf.org/doc/html/rfc7517#section-5
// no need for builder parameter type conversion overhead if this isn't present:
if (Collections.isEmpty(m) || !m.containsKey(PARAM.getId())) {
String msg = "Missing required " + PARAM + " parameter.";
throw new MalformedKeySetException(msg);
}
Object val = m.get(PARAM.getId());
if (val == null) {
String msg = "JWK Set " + PARAM + " value cannot be null.";
throw new MalformedKeySetException(msg);
}
if (!(val instanceof Collection)) {
String msg = "JWK Set " + PARAM + " value must be a Collection (JSON Array). Type found: " +
val.getClass().getName();
throw new MalformedKeySetException(msg);
}
int size = Collections.size((Collection<?>) val);
if (size == 0) {
String msg = "JWK Set " + PARAM + " collection cannot be empty.";
throw new MalformedKeySetException(msg);
}
// Copy values so we don't mutate the original input
Map<String, Object> src = new LinkedHashMap<>(Collections.size((Map<?, ?>) o));
for (Map.Entry<?, ?> entry : ((Map<?, ?>) o).entrySet()) {
Object key = Assert.notNull(entry.getKey(), "JWK Set map key cannot be null.");
if (!(key instanceof String)) {
String msg = "JWK Set map keys must be Strings. Encountered key '" + key + "' of type " +
key.getClass().getName();
throw new IllegalArgumentException(msg);
}
String skey = (String) key;
src.put(skey, entry.getValue());View on GitHub (pinned to fb71496164)
Solutions
- Wrap the single JWK in a JSON array: "keys":[{...}]
- Fix the serializer/deserializer so keys is always an array
- Validate the JWKS shape before passing it to jjwt
Example fix
// before
{"keys":{"kty":"RSA","n":"...","e":"AQAB"}}
// after
{"keys":[{"kty":"RSA","n":"...","e":"AQAB"}]} Defensive patterns
Strategy: type-guard
Validate before calling
if (!(m.get("keys") instanceof Collection)) { throw new IllegalArgumentException("'keys' must be a JSON array"); } Type guard
boolean keysIsArray(Map<?,?> m) { return m != null && m.get("keys") instanceof Collection<?>; } Try / catch
try { jwkSet = jwkSetConverter.applyFrom(map); } catch (MalformedKeySetException e) { log.error("keys must be an array: {}", e.getMessage()); } Prevention
- Always serialize single keys as a one-element array
- Round-trip JWKS documents through your serializer in tests
When it happens
Trigger: JWKS input where "keys" is a single JWK object (Map) instead of an array, or a JSON string.
Common situations: Hand-written JWKS with a single key written as an object rather than a one-element array; custom serializers flattening single-element arrays.
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
- Value must be a Map<String,?> (JSON Object). Type found: ${t
- ${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 kty value must be a String. Type found: ${type}
AI-assisted analysis of jwtk/jjwt@fb71496164 (2026-09-09).
Data as JSON: /api/errors/bbb453b8054dc3d2.
Report an issue: GitHub.