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
- Implementing Serializable and what it really means
- Writing/reading objects with ObjectOutputStream/InputStream
- Using ByteArrayOutputStream so it runs anywhere
- What transient fields do (and why)
- serialVersionUID and version compatibility
- The security risks — and JSON as a safer alternative
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.
- Never deserialize data you don't fully trust.
- For data crossing system boundaries, prefer JSON (Jackson/Gson) — language-neutral, human-readable, and far safer.
- If you must use object streams on external input, apply an ObjectInputFilter allow-list.
🧠 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
- ❌ Forgetting to implement Serializable. NotSerializableException. Fix: implement it on the class and every non-transient referenced type.
- ❌ No declared serialVersionUID. Edits silently break old data. Fix: declare private static final long serialVersionUID.
- ❌ Serializing secrets. Passwords end up in the blob. Fix: mark them transient — or don't serialize the object.
- ❌ Expecting transient fields to survive. They reset to defaults. Fix: recompute them after restore, or don't make them transient.
- ❌ Deserializing untrusted data. Remote code execution risk. Fix: use JSON, or an ObjectInputFilter allow-list.
- ❌ Ignoring the checked exceptions. readObject throws IOException and ClassNotFoundException. Fix: declare or catch them.
Pro Tips
- 💡 Prefer JSON for data exchange. It's portable across languages, debuggable by eye, and dodges the deserialization-attack surface entirely.
- 💡 Always declare serialVersionUID on any Serializable class to control compatibility deliberately.
- 💡 Use ByteArrayOutputStream to test serialization without touching the filesystem.
- 💡 Records are serializable too, but their serialization is based on components and is more robust against the classic attacks.
📋 Quick Reference
| Concept | Syntax | Note |
|---|---|---|
| mark serializable | implements Serializable | Marker interface |
| version stamp | static final long serialVersionUID | Always declare it |
| skip a field | transient | Not written |
| write object | out.writeObject(obj) | ObjectOutputStream |
| read object | (T) in.readObject() | Cast the result |
| in-memory out | ByteArrayOutputStream | No file needed |
| in-memory in | ByteArrayInputStream | Wrap the bytes |
| not serializable | NotSerializableException | Field type fails |
| version mismatch | InvalidClassException | UID differs |
| security filter | ObjectInputFilter | Allow-list classes |
| safer alternative | JSON (Jackson/Gson) | For external data |
📚 Resources / Keep Learning
- 📘 Serializable — Java 21 API documentation (docs.oracle.com)
- 📘 ObjectInputStream — Java 21 API (docs.oracle.com)
- 📘 Serialization Filtering (security) — Java 21 Core Guide (docs.oracle.com)
🎉 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
- Previous: Dependency Injection
- Next: Checkpoint: Practical Java — Combine builders, immutability, collectors and HTTP into a small app — then a timed quiz
- Quick reference: Java cheat sheet
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).