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

  1. Verify the payload decodes to a JSON object: check for '{' after base64url-decoding
  2. Reject the token as malformed by catching InvalidTokenStructure
  3. Fix the token issuer — payload must be json_encode of an associative array
  4. 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

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


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)