lcobucci/jwt · error · Lcobucci\JWT\Token\InvalidTokenStructure
claims must be an array with non-empty-string keys
Error message
claims must be an array with non-empty-string keys
What it means
Parser::parseClaims() Base64Url-decodes and JSON-decodes the claims segment and requires the result to be an array (JSON object). If it decodes to a scalar or another non-array type, InvalidTokenStructure::arrayExpected('claims') is thrown. JWT payload must be a JSON object of claims.
Solutions
- Verify the payload decodes to a JSON object: check for '{' after base64url-decoding
- Reject the token as malformed by catching InvalidTokenStructure
- Fix the token issuer — payload must be json_encode of an associative array
- Manually decode segments during debugging to confirm which part is wrong
Example fix
// before
$token = $parser->parse($jwt); // payload decodes to 'null'
// after
$payload = SodiumBase64Polyfill::base64UrlDecode(explode('.', $jwt)[1]);
if (!str_starts_with(trim($payload), '{')) {
throw new InvalidArgumentException('JWT payload must be a JSON object');
}
$token = $parser->parse($jwt); Defensive patterns
Strategy: validation
Validate before calling
$p = SodiumBase64Polyfill::base64UrlDecode(explode('.', $jwt)[1]); if (!is_array(json_decode($p, true))) { throw new InvalidArgumentException('Claims are not a JSON object'); } Try / catch
try { $token = $parser->parse($jwt); } catch (Lcobucci\JWT\InvalidTokenStructure $e) { return error_401('Malformed token claims'); } Prevention
- Ensure the issuer encodes an associative array as payload
- Reject empty/null payloads early
- Catch InvalidTokenStructure in one place (middleware) for all parser calls
When it happens
Trigger: The payload segment decodes to 'null', a number, a string, or a JSON array (e.g. base64url of '[1,2,3]') instead of an object.
Common situations: Corrupted/truncated payload segments; custom-issued tokens with array payloads; pasting the wrong base64 string into the payload slot in tests; buggy custom token generators.
Related errors
- Builder#withClaim() is meant to be used for non-registered…
- The JWT string is missing the Header part
- The JWT string is missing the Claim part
- The JWT string is missing the Signature part
- The JWT string must have two dots
AI-assisted analysis of lcobucci/jwt@375813049c (2026-09-14).
Data as JSON: /api/errors/4c6e54f2a6b545ce.
Report an issue: GitHub.
Appendix: source
Thrown at src/Token/Parser.php:116
return $header;
}
/**
* Parses the claim set from a string
*
* @param non-empty-string $data
*
* @return array<non-empty-string, mixed>
*
* @throws InvalidTokenStructure When parsed content isn't an array or contains non-parseable dates.
*/
private function parseClaims(string $data): array
{
$claims = $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data));
if (! is_array($claims)) {
throw InvalidTokenStructure::arrayExpected('claims');
}
$this->guardAgainstEmptyStringKeys($claims, 'claims');
if (array_key_exists(RegisteredClaims::AUDIENCE, $claims)) {
$claims[RegisteredClaims::AUDIENCE] = (array) $claims[RegisteredClaims::AUDIENCE];
}
foreach (RegisteredClaims::DATE_CLAIMS as $claim) {
if (! array_key_exists($claim, $claims)) {
continue;
}
$claims[$claim] = $this->convertDate($claims[$claim]);
}
return $claims;
}View on GitHub (pinned to 375813049c)