beemdevelopment/Aegis · error · ParseException
Unexpected serial version UID
Error message
Unexpected serial version UID: %d
What it means
FreeOtpImporter.parseClassDescriptor manually parses a Java serialized stream of a FreeOTP backup. It expects the stream's class descriptor to declare java.util.HashMap with the exact expected serialVersionUID (SERIAL_VERSION_UID). If the embedded long differs, it throws this ParseException, meaning the file is not the Java-serialized HashMap format this importer knows how to read.
Solutions
- Re-export the FreeOTP tokens from the original FreeOTP app version and import that file.
- Verify the selected file is the actual FreeOTP backup (a Java-serialized HashMap), not a JSON/other export.
- Check that the file is not corrupted — compare size/hash against the export source.
- If a legitimate new serialVersionUID must be supported, update SERIAL_VERSION_UID in FreeOtpImporter to match the producing version.
Example fix
// before: importing a mis-exported/wrong file
importer.importFromUri(wrongBackupUri);
// after: validate format upstream or re-export from stock FreeOTP
if (!isFreeOtpBackup(file)) { showUserError("Select a FreeOTP backup"); return; }
importer.importFromUri(freeOtpBackupUri); Defensive patterns
Strategy: try-catch
Validate before calling
// Peek at the file header; Java-serialized streams start with 0xAC 0xED
byte[] head = new byte[2];
try (InputStream is = contentResolver.openInputStream(uri)) {
if (is == null || is.read(head) != 2 || head[0] != (byte)0xAC || head[1] != (byte)0xED) {
throw new IllegalArgumentException("Not a FreeOTP (Java serialized) backup");
}
} Try / catch
try {
DatabaseImporter.EntryResult res = importer.importFromUri(uri);
} catch (DatabaseImporterException e) {
if (e.getMessage() != null && e.getMessage().contains("serial version UID")) {
showError("This backup is not compatible with this importer; re-export from stock FreeOTP.");
}
} Prevention
- Only export FreeOTP tokens using the stock FreeOTP app version.
- Validate the file's Java-serialization magic bytes before importing.
- Keep a hash of the original export to detect corruption.
- Never round-trip the backup through other tools that re-serialize it.
When it happens
Trigger: Importing a FreeOTP backup whose serialized HashMap has a serialVersionUID other than the hardcoded SERIAL_VERSION_UID — e.g. the file was produced by a different app version, a different serialization library, or is a truncated/re-exported file.
Common situations: Users export tokens from an updated or forked FreeOTP build with a changed serialVersionUID; the selected file is actually not a FreeOTP backup (wrong file chosen); the file was corrupted during transfer or partially rewritten by another tool.
Understand the failure class
Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.
Related errors
- Unexpected field type
- Expected a string object, found
- Unable to find nonce in parameters
- Not a serialized Java Object
- Expected an object, found:
AI-assisted analysis of beemdevelopment/Aegis@d6f4e5925a (2026-09-08).
Data as JSON: /api/errors/e94b9beab12e3a48.
Report an issue: GitHub.
Appendix: source
Thrown at app/src/main/java/com/beemdevelopment/aegis/importers/FreeOtpImporter.java:416
for (int i = 0; i < size; i++) {
String key = parseStringObject(inStream);
String value = parseStringObject(inStream);
map.put(key, value);
}
return map;
}
private static void parseClassDescriptor(DataInputStream inputStream)
throws IOException, ParseException {
// Check whether we're dealing with a HashMap and a version we support
String className = parseUTF(inputStream);
if (!className.equals(HashMap.class.getName())) {
throw new ParseException(String.format("Unexpected class name: %s", className));
}
long serialVersionUID = inputStream.readLong();
if (serialVersionUID != SERIAL_VERSION_UID) {
throw new ParseException(String.format("Unexpected serial version UID: %d", serialVersionUID));
}
// Read past all of the fields in the class
byte fieldDescriptor = inputStream.readByte();
if (fieldDescriptor == TC_NULL) {
return;
}
int totalFieldSkip = 0;
int fieldCount = inputStream.readUnsignedShort();
for (int i = 0; i < fieldCount; i++) {
char fieldType = (char) inputStream.readByte();
parseUTF(inputStream);
switch (fieldType) {
case 'F': // float (4 bytes)
case 'I': // int (4 bytes)
totalFieldSkip += 4;
break;
default:View on GitHub (pinned to d6f4e5925a)