Serialization in Java
Serialization turns an object graph into bytes and back. It is easy to switch on, hard to maintain, and a genuine security risk.
- What serialization is
- Writing and reading
- What is and is not written
- Every referenced object must be serializable
- serialVersionUID
- Compatible and incompatible changes
- Custom serialization
- The security problem
- What to use instead
- Where it still appears
- Common mistakes
- Best practices
- Practice
- Conclusion
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
What serialization is
Serialization converts an object, and everything it references, into a byte sequence that can be stored or transmitted. Deserialization rebuilds the objects from those bytes.
import java.io.Serializable;
public class Note implements Serializable {
private static final long serialVersionUID = 1L;
private String title;
private int views;
private transient String cachedSummary; // not written
public Note(String title, int views) {
this.title = title;
this.views = views;
}
}Serializable is a marker interface: it declares no methods and simply grants permission.
Writing and reading
Note note = new Note("Java streams", 610);
try (ObjectOutputStream out = new ObjectOutputStream(
Files.newOutputStream(Path.of("note.ser")))) {
out.writeObject(note);
}
try (ObjectInputStream in = new ObjectInputStream(
Files.newInputStream(Path.of("note.ser")))) {
Note restored = (Note) in.readObject();
}What is and is not written
| Included | Excluded |
|---|---|
| Instance fields | transient fields |
| Referenced objects, recursively | static fields |
| The class name and field metadata | Methods and constructors |
A transient field is restored to its default value: null, zero or false.
Every referenced object must be serializable
public class Note implements Serializable {
private Author author; // Author must also be Serializable
}
// Otherwise: NotSerializableException at write time, naming the offending classserialVersionUID
private static final long serialVersionUID = 1L;This identifier ties the bytes to a class version. If it is not declared, the compiler computes one from the class structure, and almost any change, adding a field, changing a modifier, produces a different value. Old data then fails to load with InvalidClassException.
Always declare serialVersionUID explicitly on a serializable class. Doing so means you decide when compatibility is broken rather than the compiler deciding by accident.Compatible and incompatible changes
| Change | Compatible |
|---|---|
| Adding a field | Yes, it reads back as the default |
| Removing a field | Yes, the stored value is ignored |
| Changing a field type | No |
| Renaming a field | No, it behaves as remove plus add |
| Renaming or moving the class | No |
| Adding or removing methods | Yes, methods are not serialized |
Custom serialization
public class Credentials implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient char[] password;
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeObject(encrypt(password));
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.password = decrypt((byte[]) in.readObject());
}
}These two private methods are found by reflection and let a class control its own format. Note that readObject is effectively a hidden constructor: it bypasses the real one, so every validation must be repeated there.
The security problem
// Never do this with untrusted bytes
Object value = new ObjectInputStream(untrustedInput).readObject();Deserialization constructs arbitrary objects and runs their readObject methods before any of your code inspects them. With the right classes on the classpath, a crafted byte stream can trigger a chain of calls that executes commands. This class of vulnerability has affected many well known systems.
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.notes.*;java.base/*;!*"); // allow list, reject everything else
ObjectInputStream in = new ObjectInputStream(input);
in.setObjectInputFilter(filter);Java 9 added serialization filters as a mitigation. They help, but the real answer is not to deserialize untrusted data at all.
What to use instead
| Need | Better option |
|---|---|
| Data exchange between systems | JSON or another explicit text format |
| Compact binary messages | A schema based binary format |
| Storing application data | A database |
| Configuration | Properties, YAML or JSON |
| Deep copying an object | A copy constructor |
// Explicit and safe: you decide the format, and nothing is constructed by surprise
public record NoteDto(String title, int views) {
public String toCsv() {
return title.replace(",", " ") + "," + views;
}
public static NoteDto fromCsv(String line) {
String[] parts = line.split(",", 2);
return new NoteDto(parts[0], Integer.parseInt(parts[1]));
}
}Where it still appears
- Distributed caches and session replication in older frameworks.
- Legacy remote method invocation.
- Existing files written years ago that still have to be read.
You will meet it, which is why it is worth understanding. Choosing it for something new needs a strong justification.
Common mistakes
- Omitting
serialVersionUIDand breaking compatibility on the next edit. - Forgetting that a referenced class must also be serializable.
- Serializing secrets, since the bytes are readable.
- Expecting
staticortransientfields to be restored. - Skipping validation in
readObject, so an invalid object appears without a constructor ever running. - Deserializing data from an untrusted source.
Best practices
- Prefer an explicit format such as JSON for anything crossing a boundary.
- If you must serialize, declare
serialVersionUIDand treat the format as a published contract. - Mark derived, cached and sensitive fields
transient. - Validate inside
readObjectexactly as a constructor would. - Apply an allow list filter whenever the input is not fully under your control.
- Never deserialize untrusted bytes.
Practice
- Serialize an object, add a field, and observe what happens with and without
serialVersionUID. - Why is a
transientfieldnullafter deserialization? - Explain why
readObjectcan produce an object the constructor would have rejected. - Replace a serialized file with a CSV or JSON representation and list the advantages.
- Describe, in general terms, why deserializing untrusted input is dangerous.
Conclusion
Serialization is convenient and expensive to live with: the class structure becomes a wire format, and untrusted input becomes a security risk. Understand it because you will meet it, and choose an explicit format for anything new.