How to Decode a JWT in Flutter and Android Without a Library
- Flutter/Dart: decode JWT payload using dart:convert (no package needed)
- Android Kotlin: decode using Android base64 utilities
- Android Java: the same approach in Java syntax
- Reading claims like sub, exp, and custom fields in mobile apps
Table of Contents
Decoding a JWT in a mobile app uses the same base64url approach as the browser — split on dots, decode the middle segment, parse JSON. Both Flutter/Dart and Android (Kotlin or Java) have built-in utilities that make this straightforward without adding any package.
Decode JWT in Flutter and Dart
Flutter includes dart:convert for base64 decoding and JSON parsing. No pub.dev package needed:
import 'dart:convert';
Map<String, dynamic> decodeJwtPayload(String token) {
final parts = token.split('.');
if (parts.length != 3) throw Exception('Invalid JWT format');
String payload = parts[1];
// Normalize base64url to base64 and add padding
payload = payload.replaceAll('-', '+').replaceAll('_', '/');
while (payload.length % 4 != 0) {
payload += '=';
}
final decoded = utf8.decode(base64Decode(payload));
return jsonDecode(decoded) as Map<String, dynamic>;
}
// Usage
final claims = decodeJwtPayload(accessToken);
final userId = claims['sub'] as String;
final exp = claims['exp'] as int;
final expDate = DateTime.fromMillisecondsSinceEpoch(exp * 1000);
print('Token expires: $expDate');
The while (payload.length % 4 != 0) loop handles the variable padding that base64url strips.
Decode JWT in Android with Kotlin
import android.util.Base64
import org.json.JSONObject
fun decodeJwtPayload(token: String): JSONObject {
val parts = token.split(".")
require(parts.size == 3) { "Invalid JWT format" }
var payload = parts[1]
.replace("-", "+")
.replace("_", "/")
// Add padding
when (payload.length % 4) {
2 -> payload += "=="
3 -> payload += "="
}
val decoded = Base64.decode(payload, Base64.DEFAULT)
return JSONObject(String(decoded, Charsets.UTF_8))
}
// Usage
val claims = decodeJwtPayload(accessToken)
val userId = claims.getString("sub")
val exp = claims.getLong("exp")
val expDate = java.util.Date(exp * 1000L)
Android's android.util.Base64 handles standard base64 decoding after the character substitution.
Decode JWT in Android with Java
import android.util.Base64;
import org.json.JSONObject;
public static JSONObject decodeJwtPayload(String token) throws Exception {
String[] parts = token.split("\.");
if (parts.length != 3) throw new IllegalArgumentException("Invalid JWT");
String payload = parts[1]
.replace("-", "+")
.replace("_", "/");
// Add padding
int mod = payload.length() % 4;
if (mod == 2) payload += "==";
else if (mod == 3) payload += "=";
byte[] decoded = Base64.decode(payload, Base64.DEFAULT);
return new JSONObject(new String(decoded, "UTF-8"));
}
// Usage
JSONObject claims = decodeJwtPayload(accessToken);
String userId = claims.getString("sub");
long exp = claims.getLong("exp");
Checking Token Expiry Before API Calls
In mobile apps, checking expiry before an API call prevents unnecessary 401 errors:
// Kotlin helper
fun isJwtExpired(token: String): Boolean {
return try {
val claims = decodeJwtPayload(token)
val exp = claims.optLong("exp", 0L)
if (exp == 0L) return false // no expiry claim
val nowSeconds = System.currentTimeMillis() / 1000L
nowSeconds > exp
} catch (e: Exception) {
true // treat malformed tokens as expired
}
}
// Before making a request
if (isJwtExpired(accessToken)) {
accessToken = refreshTokens()
}
apiService.getData(accessToken)
Add a buffer (e.g., nowSeconds > exp - 60) to refresh slightly before expiry, avoiding race conditions where a token expires mid-request.
Test Your Token in the Browser First
Paste your JWT above to inspect all claims before implementing the mobile decoder — verify the payload shape first.
Open Free JWT DecoderFrequently Asked Questions
Should I use a JWT library for Flutter like dart_jsonwebtoken?
If you only need to decode (not verify or sign), the built-in approach works fine and keeps your dependency count low. dart_jsonwebtoken is worth adding if you need signature verification or JWT generation.
Can I decode JWTs in React Native the same way?
Yes. React Native JavaScript runs in a JS engine that supports atob() (via polyfill or directly on newer versions). The same decodeJwt() function from the JavaScript post works in React Native with a base64 polyfill if needed.
Is it safe to store JWTs in Flutter SharedPreferences?
SharedPreferences on Android stores data in plaintext XML files. For access tokens, use FlutterSecureStorage instead, which uses Android Keystore and iOS Keychain for encrypted storage.

