Table of Contents


SerialVersionUID

In the previous post, we covered what Java serialization is, how to serialize a serializable class, and how to deserialize it. This post goes deeper into SerialVersionUID (SUID).

SUID matters because the serialization process checks that the SUID in the serialized stream matches the SUID of the current class. If they do not match, deserialization throws InvalidClassException.

If you do not declare an SUID, Java generates one automatically. The serialization spec does not require an explicit SUID and uses a computed hash if none is present.

So even without explicitly declaring SUID, the runtime generates one based on the class structure such as name and constructors. In the earlier examples, we omitted SUID in Article, yet the system still created one.


Does It Really Generate One?

Let’s verify it. Because SUID is derived from class structure, changing the structure between serialization and deserialization should trigger an error.

First, use the same Article class and serialize it. Then encode the byte array in Base64 and print it.

class Article implements Serializable {
    private String title;
    private String pressName;
    private String reporterName;

    public Article(String title, String pressName, String reporterName) {
        this.title = title;
        this.pressName = pressName;
        this.reporterName = reporterName;
    }

    @Override
    public String toString() {
        return String.format("title = %s, pressName = %s, reporterName = %s",
                title, pressName, reporterName);
    }
}

public class Main {
    public String serializeMethod() {
        Article article = new Article("What Is Serialization?", "Kimtaeng Daily", "Kimtaeng");
        ByteArrayOutputStream bos = new ByteArrayOutputStream();

        // this try-with-resources form is supported since Java 9
        try (bos; ObjectOutputStream oos = new ObjectOutputStream(bos)) {
            oos.writeObject(article);
        } catch (Exception e) {
            // ...Exception Handling
        }
        return Base64.getEncoder().encodeToString(baos.toByteArray());
    }

    public static void main(String[] args) {
        Main main = new Main();
        String serializedString = main.serializeMethod();
        System.out.println(serializedString);
    }
}

The output is a Base64-encoded ASCII string like this:

// encoded string
rO0ABXNyAAdBcnRpY2xlXrUf2Yf... omitted

Now add a field to Article and deserialize the previously generated data.

class Article implements Serializable {
    private String title;
    private String pressName;
    private String reporterName;
    
    // new field
    private String phoneNumber;
    
    // ... omitted
}

Deserialization now throws an exception:

java.io.InvalidClassException: Article;
    local class incompatible: stream classdesc serialVersionUID = 6824395829496368166,
    local class serialVersionUID = 1162379196231584967

This shows that SUID is generated even when you do not declare it, and that changing the class structure breaks compatibility.

So what should we do? Manage SUID ourselves?


Managing SUID

Java recommends that developers explicitly declare and manage SUID. Let’s add SerialVersionUID to Article.

class Article implements Serializable {
    // use a simple value for illustration
    private static final long serialVersionUID = 1L;

    private String title;
    private String pressName;
    private String reporterName;

    // ... omitted
} 

After adding SUID, the serialized output looks like this:

// encoded string
rO0ABXNyAAdBcnRpY2xlAAAAAAAAAAECAA... omitted

If you add a field and deserialize using the old data, the process succeeds as long as the SUID matches.

From this perspective, you should avoid serializing objects whose classes are likely to change. Even framework or library classes can change their SerialVersionUID across versions, which can cause unexpected errors.

Here are common change scenarios and how they behave:

  • Adding a field
    • With an explicit SUID, deserialization succeeds.
    • Fields that did not exist in the stream initialize to default values (for example, null).
  • Removing a field
    • Deserialization succeeds, but the field value is lost.
  • Renaming a field
    • Deserialization succeeds, but the value does not map to the renamed field.
  • Changing a field type
    • Deserialization can throw ClassCastException.
    • The same applies to changes between primitive types, such as int to double.
  • Changing access modifiers
    • Access modifier changes do not affect serialization.
  • static and transient
    • If a static field becomes non-static, the serialized value is ignored.
    • transient fields are excluded from serialization, so removing transient still does not restore values.


Next

This post covered SerialVersionUID and why it should be explicitly managed. It also showed how structural changes can break deserialization. The next post discusses the trade-offs and risks of Java serialization.