Serialization ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-11-01

serialVersionUID in Java: Class Version Compatibility

You saved an object to a file, added one method to the class — and the old file can no longer be read: readObject() throws java.io.InvalidClassException: local class incompatible. The culprit is the serialVersionUID you never declared.

serialVersionUID is the version number of a serializable class: a private static final long serialVersionUID field. The number is written to the stream together with the object, and during deserialization the JVM compares it with the number of the class on the classpath. If they match, the object is read even if the class has changed a little. If they don't, you get an InvalidClassException. If the field is not declared, the JVM computes the number itself, and that number changes with almost any edit to the class.

This lesson continues the topic of serialization and deserialization in Java, which covers Serializable, ObjectOutputStream and transient. Here you will learn how to survive a change to a class without losing data that has already been saved.

1. What is serialVersionUID

serialVersionUID is the class version identifier used by the serialization mechanism. The JVM takes it into account only when it is declared exactly like this: a static final long field named serialVersionUID. Any access modifier works, but private is recommended: the field belongs to its own class only and is not inherited.

import java.io.Serial;
import java.io.Serializable;

public class Cat implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;

    public Cat(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Cat{name='" + name + "', age=" + age + "}";
    }
}

The @Serial annotation (Java 14+) is optional. It marks the field as part of the serialization mechanism so that the IDE and the compiler (javac with the -Xlint:serial flag in recent JDKs) can check the declaration. That helps: if you declare the field as an int or without static, the JVM simply ignores it and silently computes the number on its own.

Static fields are normally not serialized: they belong to the class, not to the object. serialVersionUID is the exception: it does get into the stream, but not as part of the object state — as part of the class descriptor that ObjectOutputStream writes before the object data.

2. How the JVM checks the class version

During deserialization, ObjectInputStream first reads the class descriptor from the stream: the class name, the serialVersionUID and the list of fields with their types. Then it finds the class with the same name on the classpath and compares the numbers:

  1. The numbers differ — reading stops with InvalidClassException: local class incompatible. The fields are not even compared.
  2. The numbers match — the JVM matches the fields by name. A field present both in the stream and in the class gets the value from the stream. A field present only in the class gets its default value (null, 0, false). A field present only in the stream is skipped.
  3. If a field with the same name has changed its type, reading fails again (details in section 5).

So serialVersionUID is a promise made by the developer: "the new version of the class can read the data of the old one." As long as the number stays the same, the JVM trusts that promise and handles added and removed fields on its own.

3. What happens if you don't declare serialVersionUID

If the field is missing, the JVM computes a default number: a SHA-1 hash of the class structure. The calculation includes:

  • the class name and its modifiers;
  • the names of the implemented interfaces;
  • all fields except private static and private transient ones (name, modifiers, type);
  • whether there is a static initializer;
  • all non-private constructors and methods (name, modifiers, signature).

That is why the number changes after edits that have nothing to do with the saved data: you add a public method, make the class final or add an interface. The number also depends on synthetic members that the compiler generates for nested and anonymous classes — javac and the Eclipse compiler can compute different values for the same source code.

Let's check it. A class without serialVersionUID:

public class Cat implements Serializable {
    private String name;
    private int age;
    // constructor and toString() - as above
}

Save an object:

import java.io.*;

public class SaveCat {
    public static void main(String[] args) throws IOException {
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cat.ser"))) {
            out.writeObject(new Cat("Tom", 3));
        }
    }
}

Now add a method to Cat that does not touch any fields:

public void meow() {
    System.out.println(name + ": meow");
}

And read the old file:

import java.io.*;

public class LoadCat {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("cat.ser"))) {
            Cat cat = (Cat) in.readObject();
            System.out.println(cat);
        }
    }
}
Exception in thread "main" java.io.InvalidClassException: Cat; local class incompatible:
stream classdesc serialVersionUID = 4425893317461758209, local class serialVersionUID = -6302743921569021477

Your numbers will be different, but the point is the same: the data has not changed, yet it cannot be read. If you add private static final long serialVersionUID = 1L; to Cat from the start, the same scenario prints Cat{name='Tom', age=3}.

4. InvalidClassException: causes and fixes

java.io.InvalidClassException is a subclass of IOException thrown by ObjectInputStream when the class on the classpath does not match the data in the stream. The text after the class name tells you the cause:

Message Cause Fix
local class incompatible: stream classdesc serialVersionUID = X, local class serialVersionUID = Y The version number in the stream and in the class differ: the field is not declared and the class has changed, or the number was changed by hand Declare the old number X in the class (it is shown in the message) if the changes are compatible
incompatible types for field age A field with the same name changed its primitive type (for example, int → long) or a primitive was replaced with a reference type Restore the type, or add a new field with a different name and copy the value in readObject()
class invalid for deserialization The class no longer implements Serializable Bring back implements Serializable
Serializable incompatible with Externalizable The class was switched from Serializable to Externalizable or vice versa Old data cannot be read this way: a separate migration is needed
no valid constructor The nearest non-serializable superclass has no accessible no-arg constructor Add such a constructor to the superclass (see deserialization and inheritance)

Changing the reference type of a field (for example, String → StringBuilder) produces a different exception — ClassCastException: cannot assign instance of java.lang.String to field Cat.name of type java.lang.StringBuilder. The cause and the fix are the same as in the second row of the table.

5. Compatible and incompatible class changes

A compatible change is one after which the new version of the class reads the data of the old one (and vice versa) without exceptions. The list comes from the Java Object Serialization Specification (chapter 5.6). One important condition: all "compatible" changes work only when serialVersionUID is declared explicitly and has not changed. Without it, even adding a method breaks reading.

Change Compatible? What happens when old data is read
Add a field Yes The new field gets its default value
Add or remove a method or constructor Yes The data is not affected (with an explicit serialVersionUID)
Change a field's access modifier (private → public) Yes The value is read as usual
Make a field non-static or non-transient Yes Same as adding a field: default value
Add or remove the writeObject() / readObject() methods Yes The data is read the standard way
Add a new class to the hierarchy or remove a class from it Yes Fields of the new class get default values; data of the removed class is skipped
Delete a field Formally no The new version reads old data (the value from the stream is skipped). But an old version of the class reading new data gets the default value in that field — that is why the specification calls this change incompatible
Change a field's type No InvalidClassException: incompatible types for field (primitives) or ClassCastException (reference types)
Make a field static or transient No Same as deleting the field: the value from the stream is lost
Rename a field No For the JVM this is deleting the old field and adding a new one: the value is lost
Move the class to another package or rename it No ClassNotFoundException: the stream stores the fully qualified class name
Move the class up or down the hierarchy No The class descriptors in the stream do not match the current hierarchy
Remove implements Serializable No InvalidClassException: class invalid for deserialization
Switch from Serializable to Externalizable or vice versa No InvalidClassException: Serializable incompatible with Externalizable

Let's check both cases with the Cat class that has serialVersionUID = 1L. Save new Cat("Tom", 3), then add a field and update toString():

public class Cat implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    private String color; // new field

    @Override
    public String toString() {
        return "Cat{name='" + name + "', age=" + age + ", color=" + color + "}";
    }
}
Cat{name='Tom', age=3, color=null}

The old file was read, and the new field is null. Now, instead of adding a field, change the type of age from int to long:

Exception in thread "main" java.io.InvalidClassException: Cat; incompatible types for field age

A matching serialVersionUID does not help: the JVM cannot convert an int from the stream into a long field by itself.

6. How to change a class and keep old data

If the default value does not suit a new field, set it in the readObject() method. The JVM calls this private method during deserialization, and in.defaultReadObject() inside it reads the fields the standard way:

public class Cat implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    private String color;

    @Serial
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        if (color == null) {
            color = "unknown"; // old files have no color field
        }
    }
    // toString() - as above
}
Cat{name='Tom', age=3, color=unknown}

For incompatible changes (a new type, a renamed field, a move to another package) there is no universal trick. Your options:

  • Keep the old field and add a new one. Leave int age, add long ageInDays and fill it in readObject() from the old field.
  • Run a migration. Read the old files with a program that uses the old version of the class, save the data in the new form, and only then update the class.
  • Change the version on purpose. Increase serialVersionUID (for example, to 2L) if the old data is no longer needed: an attempt to read it fails right away with a clear error instead of producing a silently broken object.

If the data must live for a long time or be read by other programs, it is safer to store it not in the built-in Java format but in JSON (Jackson, Gson) or Protobuf: their schema evolution rules are simpler and do not depend on the structure of a Java class.

7. Which value to use: 1L or a generated one

Any value works: the JVM only checks that the numbers match. There are two typical cases:

  • A new class that has never been serialized — use 1L. It is simple, reads as "version 1", and is easy to bump to 2L after an incompatible change.
  • An old class without serialVersionUID whose data already sits in files or a cache — declare the number the JVM used to compute. Otherwise you will break reading yourself. Take the number from the InvalidClassException message (the stream classdesc value) or run the JDK serialver tool on the old version of the class:
serialver -classpath . Cat
Cat:    private static final long serialVersionUID = 4425893317461758209L;

IntelliJ IDEA can generate this value too: enable the Serializable class without 'serialVersionUID' inspection and apply the quick-fix (Alt+Enter).

8. serialVersionUID in a hierarchy, enums and records

Hierarchy. Every serializable class is checked separately: the stream contains descriptors of the object's class and of all its serializable superclasses. If Cat extends Animal and both implement Serializable, declare serialVersionUID in each of them. Changing the number in Animal breaks reading of all saved Cat objects, even if nothing changed in Cat itself. Declaring the number once in the superclass "for everyone" is useless: the JVM looks for the field only in the class itself, not in its ancestors.

enum. Enums are serialized by constant name, and their serialVersionUID is always 0L. There is no need to declare it — the JVM ignores the declaration anyway.

record. Records (Java 16+) are deserialized through the canonical constructor. The serialVersionUID matching check is waived for them, so you don't have to declare the number.

A subtle point

A matching serialVersionUID does not guarantee that the object is read correctly. It only lets the JVM try. If you changed the meaning of a field (for example, price used to store dollars and now stores cents), old data is read without errors but with wrong values. Change the number when the meaning of the data changes, not only its structure.

9. IDE and compiler warnings

Many developers first meet serialVersionUID not in their own code but in an IDE warning. It usually shows up on a custom exception or a Swing class: Throwable and Component implement Serializable, so all their subclasses are serializable too.

  • Eclipse: The serializable class MyException does not declare a static final serialVersionUID field of type long.
  • IntelliJ IDEA: the Serializable class without 'serialVersionUID' inspection (off by default).
  • javac: with the -Xlint:serial flag — serializable class MyException has no definition of serialVersionUID.
public class OrderNotFoundException extends RuntimeException {
    @Serial
    private static final long serialVersionUID = 1L;

    public OrderNotFoundException(String message) {
        super(message);
    }
}

If objects of the class are never serialized, you can safely suppress the warning with @SuppressWarnings("serial"). But declaring serialVersionUID = 1L is almost as short, and it protects the class if someone starts serializing it later (for example, the exception is sent over RMI or stored in an HTTP session).

Frequently asked questions

Is it mandatory to declare serialVersionUID?

No, the code compiles without it: the JVM computes the number itself. But the Java documentation strongly recommends declaring it explicitly: the computed number changes with almost any edit to the class and can differ between compilers, which leads to InvalidClassException when old data is read.

Do I have to change serialVersionUID after adding a field?

No. Adding a field is a compatible change: old data is still read, and the new field gets its default value. Change the number only when old data cannot or should not be read by the new version of the class.

Is serialVersionUID inherited?

No. The JVM looks for the serialVersionUID field only in the class itself. If a subclass does not declare it, the subclass gets a computed default number, even if its superclass declares its own. That is why the number is declared in every serializable class of the hierarchy.

Why is serialVersionUID written to the stream if static fields are not serialized?

It is written not as object state but as part of the class descriptor that ObjectOutputStream writes before the data. Other static fields do not get into the stream.

Can serialVersionUID be negative or non-private?

Yes. Any long value works, including negative ones (generated numbers are often negative), and so does any access modifier. Only static, final and the long type are required. private is recommended because only the class itself needs the field.

Comments

Please log in or register to have a possibility to add comment.