Serialization (Serializable, transient)

Reviewed & published by Brayan K

Serialization is the process of converting a Java object into a stream of bytes that can be stored or transmitted, and later turned back into an equal object — a powerful but security-sensitive part of the platform.

Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

Before You Start

You should know classes and objects, interfaces (Serializable is one), and have seen exceptions — these stream operations throw several checked ones.

What You'll Learn

The Big Idea — Flat-Packing Furniture

💡 Analogy: Serialization is like flat-packing a piece of furniture to ship it. The assembled chair (your object) is broken down into a flat box of parts (bytes) that can be stored or posted, then rebuilt into an identical chair at the other end (deserialization). transient fields are the parts you deliberately leave out of the box — the cushions you'll buy fresh on arrival. And serialVersionUID is the model number printed on the box: if the assembly instructions at the destination are for a different model number, you refuse to build it rather than produce a broken chair.

All examples below flat-pack into an in-memory ByteArrayOutputStream instead of a file, so they run anywhere with no disk access — and print the real round-tripped result.

1️⃣ A Basic Round-Trip

Implement Serializable, declare a serialVersionUID, then write with ObjectOutputStream.writeObject and read back with ObjectInputStream.readObject (casting the result). We serialize into a byte array so the example needs no file.

import java.io.*;

public class Main {
    // A serializable class: implements Serializable and declares serialVersionUID.
    static final class Person implements Serializable {
        private static final long serialVersionUID = 1L;
        private final String name;
        private final int age;
        Person(String name, int age) { this.name = name; this.age = age; }
        @Override public String toString() { return "Person{name=" + name + ", age=" + age + "}"; }
    }

    public static void main(String[] args) throws Exception {
        Person original = new Person("Ana", 30);

        // SERIALIZE to an in-memory byte array (no file needed -> runs anywhere).
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        try (ObjectOutputStream out = new ObjectOutputStream(bytes)) {
            out.writeObject(original);
        }
        byte[] data = bytes.toByteArray();
        System.out.println("Serialized " + data.length + " bytes");

        // DESERIALIZE back into a new object.
        try (ObjectInputStream in =
                     new ObjectInputStream(new ByteArrayInputStream(data))) {
            Person restored = (Person) in.readObject();
            System.out.println("Original : " + original);
            System.out.println("Restored : " + restored);
            System.out.println("Same data, different object? "
                    + (original != restored));
        }
    }
}

2️⃣ transient — Fields That Don't Travel

Mark a field transient to keep it out of the byte stream — for secrets like a password, recomputable caches, or non-serializable references. After deserialization those fields hold their default value (null for objects, 0 for numbers).

import java.io.*;

public class Main {
    // 'transient' fields are SKIPPED during serialization.
    static final class Account implements Serializable {
        private static final long serialVersionUID = 1L;
        private final String user;
        private transient String password;        // never written to bytes
        private transient int loginAttempts;      // reset on restore

        Account(String user, String password) {
            this.user = user;
            this.password = password;
            this.loginAttempts = 3;
        }
        @Override public String toString() {
            return "Account{user=" + user + ", password=" + password
                    + ", loginAttempts=" + loginAttempts + "}";
        }
    }

    public static void main(String[] args) throws Exception {
        Account original = new Account("ana", "s3cr3t");
        System.out.println("Before: " + original);

        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        try (ObjectOutputStream out = new ObjectOutputStream(bytes)) {
            out.writeObject(original);
        }
        try (ObjectInputStream in =
                     new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
            Account restored = (Account) in.readObject();
            // transient fields come back as defaults: null and 0.
            System.out.println("After : " + restored);
        }
    }
}

3️⃣ serialVersionUID & Compatibility

The serialVersionUID is a version stamp. On read, Java compares the stream's UID with the current class's; a mismatch throws InvalidClassException. Always declare it explicitly — otherwise the compiler auto-generates one that changes on almost any edit, silently breaking old data.

⚠️ The Big Security Warning

Java's native deserialization is one of the platform's most exploited features. Deserializing bytes from an untrusted source can instantiate arbitrary classes and execute code during reconstruction — so-called gadget chains that have produced many real-world remote-code-execution CVEs.

🧠 Quick Recall

Predict before you peek.

Answer: 0 — its type default. Transient fields aren't written, so on restore objects are null and numbers are 0/0.0.

Answer: NotSerializableException at write time, naming the offending class. Make that type Serializable or mark the field transient.

Answer: not with native Java serialization — untrusted streams can trigger code execution. Use JSON for external data, or a strict ObjectInputFilter if you truly must.

🎯 YOUR TURN — Skip a cache field

Add a transient cache field to Book so the title persists but the cache comes back null.

import java.io.*;

public class Main {
    static final class Book implements Serializable {
        private static final long serialVersionUID = 1L;
        private final String title;

        // YOUR TURN: add a field 'cachedSummary' that should NOT be persisted
        // (it's an expensive-to-build cache). Mark it transient. After a
        // round-trip the title survives but cachedSummary comes back null.
        //
        // Expected output:
        //   Before: Book{title=Java, cachedSummary=a long expensive-to-build summary}
        //   After : Book{title=Java, cachedSummary=null}

        // private final transient String cachedSummary;

        Book(String title, String cachedSummary) {
            this.title = title;
            // this.cachedSummary = cachedSummary;
        }
        public String toString() { return "Book{title=" + title + "}"; }  // update me
    }

    public static void main(String[] args) throws Exception {
        Book b = new Book("Java", "a long expensive-to-build summary");
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try (ObjectOutputStream oos = new ObjectOutputStream(out)) { oos.writeObject(b); }
        try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(out.toByteArray()))) {
            Book r = (Book) ois.readObject();
            System.out.println("Before: " + b);
            System.out.println("After : " + r);
        }
    }
}

🧩 MINI-CHALLENGE — Generic round-trip helper

Write using in-memory streams, and prove the restored Cart has equal contents but is a different object.

import java.io.*;
import java.util.*;

public class Main {
    static final class Cart implements Serializable {
        private static final long serialVersionUID = 7L;
        private final String owner;
        private final List<String> items;
        Cart(String owner, List<String> items) {
            this.owner = owner;
            this.items = List.copyOf(items);
        }
        public String toString() { return "Cart{owner=" + owner + ", items=" + items + "}"; }
    }

    // MINI-CHALLENGE: write a generic roundTrip(T) helper that serializes an
    // object to an in-memory byte array and deserializes it back, returning a
    // NEW equal object. Use it on a Cart and confirm the contents match but
    // the reference differs.
    //
    // Expected output:
    //   Original : Cart{owner=ana, items=[book, pen, lamp]}
    //   Restored : Cart{owner=ana, items=[book, pen, lamp]}
    //   Equal contents? true
    //   Different object? true

    public static void main(String[] args) throws Exception {
        Cart original = new Cart("ana", List.of("book", "pen", "lamp"));
        // Cart restored = roundTrip(original);
        // print as above
    }
}

Common Errors

Pro Tips

📋 Quick Reference

ConceptSyntaxNote
mark serializableimplements SerializableMarker interface
version stampstatic final long serialVersionUIDAlways declare it
skip a fieldtransientNot written
write objectout.writeObject(obj)ObjectOutputStream
read object(T) in.readObject()Cast the result
in-memory outByteArrayOutputStreamNo file needed
in-memory inByteArrayInputStreamWrap the bytes
not serializableNotSerializableExceptionField type fails
version mismatchInvalidClassExceptionUID differs
security filterObjectInputFilterAllow-list classes
safer alternativeJSON (Jackson/Gson)For external data

📚 Resources / Keep Learning

🎉 Lesson Complete!

You can now make a class Serializable, round-trip it through ObjectOutputStream/ObjectInputStream using in-memory byte arrays, control what travels with transient, manage compatibility with serialVersionUID, and — crucially — respect the deserialization security risks by preferring JSON for untrusted data.

Practice quiz

What is serialization?

  • Sorting objects
  • Encrypting data
  • Converting an object into a byte stream
  • Compiling code

Answer: Converting an object into a byte stream. Serialization turns an object's state into a sequence of bytes that can be stored or transmitted, and later reconstructed (deserialized).

Which interface marks a class as serializable?

  • Serializable
  • Comparable
  • Cloneable
  • Runnable

Answer: Serializable. A class must implement java.io.Serializable (a marker interface with no methods) to be serialized by ObjectOutputStream.

Which stream writes an object to bytes?

  • ObjectInputStream
  • PrintStream
  • DataOutputStream
  • ObjectOutputStream

Answer: ObjectOutputStream. ObjectOutputStream.writeObject(obj) serializes; ObjectInputStream.readObject() deserializes.

What does the transient keyword do to a field?

  • Makes it final
  • Excludes it from serialization
  • Makes it static
  • Encrypts it

Answer: Excludes it from serialization. A transient field is skipped during serialization and comes back as its default value (null or 0) after deserialization.

What is serialVersionUID for?

  • Version compatibility between serialized and current class
  • Object identity
  • Thread safety
  • Hashing

Answer: Version compatibility between serialized and current class. It identifies a class version; if a stream's UID doesn't match the loaded class, deserialization throws InvalidClassException.

What happens to a transient field after deserialization?

  • Keeps its old value
  • Throws an exception
  • Becomes its default (null/0)
  • Becomes final

Answer: Becomes its default (null/0). Transient fields are not written, so on restore they hold the type's default — null for objects, 0 for numbers.

Why is Java deserialization a security risk?

  • It's slow
  • Untrusted byte streams can be crafted to execute malicious code (gadget chains)
  • It uses too much memory
  • It's deprecated entirely

Answer: Untrusted byte streams can be crafted to execute malicious code (gadget chains). Deserializing untrusted data can trigger 'gadget chains' leading to remote code execution — never deserialize data you don't trust.

A safer modern alternative to Java serialization for data exchange is...

  • Object streams
  • transient
  • Reflection
  • JSON (e.g. Jackson/Gson)

Answer: JSON (e.g. Jackson/Gson). Text formats like JSON are language-neutral, human-readable, and far safer for exchanging data across systems.

If you serialize a class that references another object, that object must also...

  • be transient
  • implement Serializable (or be transient)
  • be final
  • be static

Answer: implement Serializable (or be transient). All non-transient referenced objects must themselves be Serializable, or you get NotSerializableException.

What does ObjectInputStream.readObject() return?

  • A String
  • void
  • An Object you must cast
  • An int

Answer: An Object you must cast. readObject() returns Object, so you cast it to the expected type — and it can throw ClassNotFoundException.

Continue this course

Frequently asked questions

What is serialization and when is it used?

Serialization is the process of converting an object's in-memory state into a byte stream, so it can be saved to disk, cached, or sent across a network — and deserialization reconstructs the object from those bytes. Java's built-in mechanism uses ObjectOutputStream and ObjectInputStream and only works on classes that implement Serializable. Historically it powered things like HTTP session persistence, RMI, and caching. For exchanging data between different systems or languages, text formats like JSON have largely replaced it.

What does implementing Serializable actually do?

Serializable is a marker interface — it declares no methods. Its mere presence tells the JVM that ObjectOutputStream is allowed to serialize instances of the class. If you try to serialize an object whose class does not implement Serializable, you get a NotSerializableException. The same requirement applies transitively: every non-transient field that references another object must also be Serializable, or serialization fails on that field.

What are transient fields for?

Marking a field transient excludes it from serialization. You use it for data that shouldn't or can't be persisted: sensitive values like passwords, caches and derived values you can recompute, or non-serializable references (like a database connection or a thread). After deserialization a transient field holds the default value for its type — null for object references, 0 for numbers, false for booleans — so your class should be able to rebuild or tolerate that state.

Why should I declare serialVersionUID?

serialVersionUID is a version number for the serialized form of a class. When deserializing, Java compares the UID stored in the stream with the UID of the loaded class; if they differ, it throws InvalidClassException. If you don't declare one, the compiler generates it from the class structure, so almost any change (adding a field, reordering) silently changes it and breaks old data. Declaring an explicit 'private static final long serialVersionUID' gives you control over when compatibility is intentionally broken.

Why is Java deserialization considered a security risk?

Deserializing data from an untrusted source is dangerous because readObject can instantiate arbitrary classes and run code during reconstruction. Attackers craft malicious byte streams — known as 'gadget chains' — that, when deserialized, trigger a sequence of method calls leading to remote code execution, even if your own class looks harmless. This has caused many real CVEs. The rules: never deserialize data you don't fully trust, prefer safer formats like JSON for external data, and if you must use it, apply serialization filters (ObjectInputFilter).

Related lessons