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

  1. Re-export the FreeOTP tokens from the original FreeOTP app version and import that file.
  2. Verify the selected file is the actual FreeOTP backup (a Java-serialized HashMap), not a JSON/other export.
  3. Check that the file is not corrupted — compare size/hash against the export source.
  4. 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

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


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)