Skip to content
Java serialization 7 min read

Serialization

Serialization is the process of converting a Java object into a stream of bytes so it can be saved to a file, sent over a network, or stored in a database — and then reconstructed (deserialized) back into an identical object later. It is one of Java’s oldest persistence mechanisms and still widely used in caching systems, RMI, and distributed computing.

Why Serialization Matters

Imagine you have a User object with a name, email, and preferences. Without serialization, that object only exists in memory for the lifetime of your program. Serialization lets you:

  • Persist objects to disk and reload them later
  • Transmit objects across a network (e.g., RMI, sockets)
  • Cache complex object graphs and restore them quickly
  • Deep-copy objects by serializing and immediately deserializing them

Making a Class Serializable

To make a class serializable, implement the java.io.Serializable interface. It is a marker interface — it has no methods. You are simply telling the JVM “this class is safe to serialize.”

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;

    private String name;
    private String email;
    private int age;

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

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

Note: Always declare a private static final long serialVersionUID. If you don’t, the JVM generates one automatically based on the class structure — and any change to the class (adding a field, changing a method) will generate a different UID, causing InvalidClassException when you try to deserialize old data.

Serializing an Object

Use ObjectOutputStream wrapped around a FileOutputStream (or any OutputStream) to write an object:

import java.io.*;

public class SerializeDemo {
    public static void main(String[] args) throws IOException {
        User user = new User("Alice", "[email protected]", 30);

        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("user.ser"))) {
            oos.writeObject(user);
            System.out.println("Object serialized: " + user);
        }
    }
}

Output:

Object serialized: User{name='Alice', email='[email protected]', age=30}

The file user.ser now contains the binary representation of the User object.

Deserializing an Object

Use ObjectInputStream to read the object back. The class must be on the classpath at deserialization time:

import java.io.*;

public class DeserializeDemo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("user.ser"))) {
            User user = (User) ois.readObject();
            System.out.println("Object deserialized: " + user);
        }
    }
}

Output:

Object deserialized: User{name='Alice', email='[email protected]', age=30}

Tip: Always use try-with-resources when working with ObjectOutputStream and ObjectInputStream — they implement Closeable and must be properly closed to flush the data and release file handles.

serialVersionUID Explained

The serialVersionUID is a version stamp embedded in the serialized byte stream. When deserializing, Java checks that the UID in the stream matches the UID of the loaded class. If they don’t match, deserialization fails with InvalidClassException.

// Explicit UID — stable across changes you consider backward-compatible
private static final long serialVersionUID = 42L;
ScenarioResult
Same class, same UIDDeserialization succeeds
Class changed, UID unchanged (your choice)Succeeds; new fields get default values
Class changed, UID auto-regenerated by JVMInvalidClassException — stream UID mismatch
No serialVersionUID declaredJVM computes it — fragile, avoid this

Warning: If you rely on serialized data surviving across deployments (files on disk, network caches), always declare an explicit serialVersionUID and increment it intentionally when making breaking changes.

What Gets Serialized?

Serialization captures the instance state — the values of all non-static, non-transient fields. Here is what is included and excluded:

IncludedExcluded
Instance fields of the objectstatic fields (class-level, not instance state)
Fields of superclasses (if also Serializable)transient fields (explicitly excluded)
Nested/referenced objects (if also Serializable)Methods and constructors

If any field references an object whose class does not implement Serializable, you will get a NotSerializableException at runtime.

The transient Keyword

Mark a field transient to exclude it from serialization. This is useful for passwords, open file handles, cached computed values, or anything that should not leave the JVM:

public class Session implements Serializable {
    private static final long serialVersionUID = 1L;

    private String sessionId;
    private transient String cachedToken; // NOT serialized
    private transient java.sql.Connection dbConnection; // NOT serialized

    public Session(String sessionId, String cachedToken) {
        this.sessionId = sessionId;
        this.cachedToken = cachedToken;
    }

    @Override
    public String toString() {
        return "Session{id='" + sessionId + "', token='" + cachedToken + "'}";
    }
}

When deserialized, cachedToken will be null (or 0 / false for primitives). See the transient Keyword page for full details and patterns for reinitializing transient fields after deserialization.

Lifecycle Hooks: readObject and writeObject

You can customize serialization behavior by declaring private writeObject and readObject methods in your class. The JVM calls them automatically:

import java.io.*;

public class SecureUser implements Serializable {
    private static final long serialVersionUID = 1L;

    private String name;
    private transient String password; // never serialize raw password

    public SecureUser(String name, String password) {
        this.name = name;
        this.password = password;
    }

    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject(); // serialize non-transient fields normally
        // write a hashed version instead of the raw password
        oos.writeObject(hash(password));
    }

    private void readObject(ObjectInputStream ois)
            throws IOException, ClassNotFoundException {
        ois.defaultReadObject(); // deserialize non-transient fields
        String hashedPassword = (String) ois.readObject();
        // restore transient field from the hashed value
        this.password = "[hashed:" + hashedPassword + "]";
    }

    private String hash(String input) {
        // simplified — use BCrypt or SHA-256 in real code
        return Integer.toHexString(input.hashCode());
    }
}

Tip: writeReplace() and readResolve() are two more lifecycle hooks. readResolve() is commonly used in singleton patterns to ensure deserialization returns the existing singleton instance rather than creating a new one.

Serializing Object Graphs

Serialization is smart about object graphs. If two fields reference the same object, Java serializes it only once and restores the reference correctly on deserialization:

User alice = new User("Alice", "[email protected]", 30);
List<User> team = new ArrayList<>();
team.add(alice);
team.add(alice); // same reference twice

After round-tripping through serialization, both entries in team will still point to the same User object. This also means that circular references (A references B, B references A) are handled without infinite loops.

Under the Hood

When you call oos.writeObject(obj), the JVM:

  1. Checks that the object’s class implements Serializable (or Externalizable). Throws NotSerializableException if not.
  2. Assigns a handle to the object. If the same object appears again later in the stream, only the handle is written (no duplication).
  3. Writes class metadata — the fully qualified class name, serialVersionUID, and field descriptors — into the stream header.
  4. Recursively serializes each non-static, non-transient field, following the object graph.
  5. Calls writeObject if defined, allowing custom serialization logic.

On deserialization, readObject reverses this: reads the class descriptor, verifies the UID, allocates the object without calling any constructor, then restores field values. The fact that no constructor is called is important — your constructor validation logic does not run during deserialization.

The binary format is described in the Java Object Serialization Specification. Each serialized file starts with the magic bytes 0xACED 0x0005.

Security Considerations

Deserialization is a well-known attack surface. Never deserialize data from an untrusted source without validation:

  • An attacker can craft a malicious byte stream that triggers arbitrary code execution through gadget chains in your classpath.
  • Java 9+ introduced a serialization filter mechanism (ObjectInputFilter) that lets you whitelist or blacklist classes during deserialization.
ObjectInputStream ois = new ObjectInputStream(inputStream);
ois.setObjectInputFilter(info -> {
    if (info.serialClass() != null &&
        !info.serialClass().getName().startsWith("com.myapp.")) {
        return ObjectInputFilter.Status.REJECTED;
    }
    return ObjectInputFilter.Status.ALLOWED;
});

Warning: Deserializing data from untrusted sources (HTTP requests, user-uploaded files, external queues) without a serialization filter is a critical security vulnerability. Prefer JSON/XML serialization libraries (Jackson, Gson) for data exchange with external systems.

Externalizable: Full Control

If you need complete control over the serialization format and performance matters (e.g., you are serializing millions of objects), implement Externalizable instead of Serializable:

import java.io.*;

public class Point implements Externalizable {
    private int x, y;

    // Required public no-arg constructor
    public Point() {}

    public Point(int x, int y) { this.x = x; this.y = y; }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(x);
        out.writeInt(y);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException {
        x = in.readInt();
        y = in.readInt();
    }
}
FeatureSerializableExternalizable
Implementation effortLow — marker interfaceHigh — must implement read/write
Control over formatLimited (hooks only)Complete
PerformanceModerateCan be very fast
Constructor called on deserializationNoYes (no-arg constructor)
Default field handlingAutomaticManual

In This Section

  • transient Keyword — exclude sensitive or non-serializable fields from the byte stream and learn patterns for reinitializing them after deserialization.
  • Object Streams — the ObjectInputStream and ObjectOutputStream classes that power serialization under the hood.
  • transient Keyword — control exactly which fields are excluded from serialization.
  • File Handling — persist serialized byte streams to disk using Java’s file I/O APIs.
  • Java I/O — the broader I/O framework that serialization streams plug into.
  • RMI — Remote Method Invocation, which uses serialization to pass objects between JVMs over a network.
  • NIO.2: Path & Files — a modern alternative for file operations alongside classic serialization workflows.
Last updated June 13, 2026
Was this helpful?