Serialization and Deserialization in Java
Serialization converts an object, and everything it references, into a stream of bytes that can be saved to a file, stored in a cache or sent over a network. Deserialization rebuilds an equivalent object from those bytes, possibly in another JVM. Java's built-in mechanism is enabled by implementing java.io.Serializable and uses ObjectOutputStream and ObjectInputStream.
This tutorial covers the basics, serialVersionUID, transient fields, inheritance rules, custom serialization, records, Externalizable, and the security concerns that make modern applications prefer formats such as JSON.
1. How Serialization Works
- The class implements
Serializable, a marker interface with no methods. ObjectOutputStream.writeObject()writes the object's class description and the values of its non-static, non-transient fields, recursively following references (the object graph).ObjectInputStream.readObject()reads them back and creates a new object without calling its constructor.
2. Serializing to a File and Back
import java.io.*;
import java.nio.file.*;
import java.util.List;
public class SerializeToFile {
static class Customer implements Serializable {
private static final long serialVersionUID = 1L; // version of the serialized form
private final String name;
private final int loyaltyPoints;
private final List<String> orders;
private transient String sessionToken; // not serialized
Customer(String name, int points, List<String> orders, String token) {
this.name = name; this.loyaltyPoints = points; this.orders = orders; this.sessionToken = token;
}
public String toString() {
return "Customer[" + name + ", points=" + loyaltyPoints + ", orders=" + orders + ", token=" + sessionToken + "]";
}
}
public static void main(String[] args) throws Exception {
Path file = Path.of(System.getProperty("java.io.tmpdir"), "customer.ser");
Customer original = new Customer("Asha", 1250, List.of("A-17", "B-42"), "tok-9f2c");
try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(file))) {
out.writeObject(original); // serialization
}
System.out.println("Saved to " + file.getFileName());
try (ObjectInputStream in = new ObjectInputStream(Files.newInputStream(file))) {
Customer copy = (Customer) in.readObject(); // deserialization
System.out.println("Original: " + original);
System.out.println("Restored: " + copy);
System.out.println("Same object? " + (copy == original));
}
Files.delete(file);
}
}
Saved to customer.ser Original: Customer[Asha, points=1250, orders=[A-17, B-42], token=tok-9f2c] Restored: Customer[Asha, points=1250, orders=[A-17, B-42], token=null] Same object? false
- The list of orders was serialized automatically because
List.of(...)lists are serializable. - The
transienttoken was skipped and came back asnull. - The restored object is a new, equal-looking copy, not the same instance.
3. serialVersionUID
Every serializable class has a version number, serialVersionUID. During deserialization the stored value must match the class's current value, or an InvalidClassException is thrown.
- If you do not declare it, the JVM computes one from the class structure, so harmless changes such as adding a method can break compatibility.
- Declare it explicitly:
private static final long serialVersionUID = 1L; - Change it only when you make an incompatible change to the serialized form.
4. Serialization and Inheritance
If a superclass is not serializable, its fields are not saved. During deserialization its no-argument constructor runs to initialise them, so it must exist and be accessible:
import java.io.*;
public class InheritanceSerialization {
static class Person { // NOT Serializable
String name = "default";
Person() { System.out.println(" Person() constructor runs"); }
}
static class Employee extends Person implements Serializable {
private static final long serialVersionUID = 1L;
int empId;
Employee(String name, int empId) { this.name = name; this.empId = empId; }
}
public static void main(String[] args) throws Exception {
System.out.println("1. new Employee(...):");
Employee e = new Employee("Ravi", 501);
System.out.println("2. deserializing a copy:");
Employee copy = roundTrip(e);
System.out.println("empId (serializable class field): " + copy.empId);
System.out.println("name (non-serializable parent): " + copy.name);
}
@SuppressWarnings("unchecked")
static <T> T roundTrip(T obj) throws Exception {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(bytes)) { out.writeObject(obj); }
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
return (T) in.readObject();
}
}
}
1. new Employee(...): Person() constructor runs 2. deserializing a copy: Person() constructor runs empId (serializable class field): 501 name (non-serializable parent): default
The name set by the Employee constructor was lost because Person is not serializable. If a superclass is serializable, all subclasses are automatically serializable too.
5. NotSerializableException
Every object in the graph must be serializable (or the field holding it must be transient):
import java.io.*;
public class NotSerializableDemo {
static class Address { String city = "Pune"; } // not Serializable
static class User implements Serializable {
private static final long serialVersionUID = 1L;
String name = "Meera";
Address address = new Address(); // breaks serialization
}
public static void main(String[] args) {
try {
InheritanceSerialization.roundTrip(new User());
} catch (Exception e) {
System.out.println(e);
}
}
}
java.io.NotSerializableException: NotSerializableDemo$Address
6. Custom Serialization and Records
Private writeObject and readObject methods let a class control its serialized form, for example to encode sensitive fields or validate data on the way in. Records are deserialized through their canonical constructor, so validation there always applies:
import java.io.*;
import java.util.Base64;
public class CustomSerialization {
static class Account implements Serializable {
private static final long serialVersionUID = 1L;
private final String user;
private transient String pin; // handled manually below
Account(String user, String pin) { this.user = user; this.pin = pin; }
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // write normal fields
out.writeUTF(Base64.getEncoder().encodeToString(pin.getBytes())); // "encrypt" (demo only)
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
pin = new String(Base64.getDecoder().decode(in.readUTF()));
if (user == null || user.isBlank()) throw new InvalidObjectException("user missing"); // validate
}
public String toString() { return user + " / pin length " + pin.length(); }
}
record Point(int x, int y) implements Serializable {
Point { // records deserialize via this constructor
if (x < 0 || y < 0) throw new IllegalArgumentException("negative coordinate");
}
}
public static void main(String[] args) throws Exception {
System.out.println(InheritanceSerialization.roundTrip(new Account("asha", "4821")));
System.out.println(InheritanceSerialization.roundTrip(new Point(3, 4)));
}
}
asha / pin length 4 Point[x=3, y=4]
Other hooks include writeReplace/readResolve (substitute another object, as singletons do) and serialPersistentFields.
7. Serializable vs Externalizable
| Aspect | Serializable | Externalizable |
|---|---|---|
| Methods to implement | None (marker) | writeExternal and readExternal |
| What is written | All non-transient fields, automatically | Only what you write explicitly |
| Constructor on read | Not called (except non-serializable parents) | Public no-arg constructor required and called |
transient | Respected | Irrelevant |
8. Security and Modern Alternatives
Deserializing untrusted data is dangerous: crafted byte streams can trigger "gadget chains" in classes on the classpath and lead to remote code execution. This has caused many serious vulnerabilities.
- Never deserialize data from untrusted sources with
ObjectInputStream. - If you must, use deserialization filters (
ObjectInputFilter, Java 9+, and context-specific filters in Java 17) to allow only expected classes. - For data exchange and storage, prefer JSON (Jackson, Gson), Protocol Buffers, Avro or similar formats, which are language-neutral and safer.
9. Summary
- Serialization turns an object graph into bytes; deserialization creates a new object from them.
- Implement
Serializable, declare aserialVersionUID, and mark fields that should not be saved astransient. - Non-serializable superclasses need a no-arg constructor; non-serializable fields cause
NotSerializableException. - Customise with
writeObject/readObject; records deserialize through their constructor. - Never deserialize untrusted input; prefer JSON or binary schema formats for data exchange.
Related Tutorials
Frequently Asked Questions (FAQ)
Q1. What is serialization in Java?
The process of converting an object's state into a byte stream so it can be stored or transmitted, using ObjectOutputStream.
Q2. What is deserialization?
Reconstructing an object from a byte stream with ObjectInputStream.readObject().
Q3. Why is Serializable a marker interface?
It has no methods; it only marks a class as allowed to be serialized. ObjectOutputStream checks for it.
Q4. What is serialVersionUID?
A version identifier for a serializable class. Deserialization fails with InvalidClassException if the stored and current values differ.
Q5. What happens if I don't declare serialVersionUID?
The JVM computes one from the class structure, so even minor changes may make previously serialized data unreadable.
Q6. Are static fields serialized?
No. They belong to the class, not the object.
Q7. Are transient fields serialized?
No. They are skipped and receive default values when the object is deserialized.
Q8. Is the constructor called during deserialization?
Not for serializable classes. Only the no-arg constructor of the first non-serializable superclass runs. Records are the exception: their canonical constructor is used.
Q9. What causes NotSerializableException?
An object in the graph whose class does not implement Serializable, for example a field of a non-serializable type.
Q10. If a parent class is Serializable, is the child too?
Yes. Serializability is inherited.
Q11. What is Externalizable?
An interface extending Serializable that gives full control: you implement writeExternal and readExternal yourself.
Q12. What are writeObject and readObject used for?
Private hooks for customising serialization: encrypting or compressing fields, writing derived data, or validating input on deserialization.
Q13. What is readResolve?
A method that returns a replacement object after deserialization, commonly used to preserve singletons.
Q14. Why is Java deserialization a security risk?
Malicious byte streams can instantiate unexpected classes and chain their methods to execute code. Untrusted data must never be deserialized without strict filtering.
Q15. What is ObjectInputFilter?
A Java 9+ API for allowing or rejecting classes, array sizes and graph depth during deserialization, reducing attack surface.
Q16. Are collections serializable?
Standard implementations such as ArrayList and HashMap are, provided their elements are serializable too.
Q17. Can I serialize a lambda?
Only if its target type is serializable (for example an intersection cast with Serializable), and it is strongly discouraged.
Q18. Is Java serialization used in modern applications?
Less and less. JSON, Protocol Buffers and similar formats are preferred for APIs, messaging and storage; Java serialization survives mainly in legacy systems and some caches.
Q19. Does serialization preserve object identity?
Within one stream, shared references are preserved: an object referenced twice is written once. The deserialized objects are new instances, distinct from the originals.
Q20. Can I deep-copy an object with serialization?
Yes, by serializing and deserializing it, but it is slow and requires everything to be serializable. Copy constructors are usually better.