Contents
- Basic Serialization
- transient and serialVersionUID
- Custom readObject and writeObject
- Externalizable — Full Control
- Security Considerations
A class becomes serializable by implementing Serializable, which is a marker interface with no methods. ObjectOutputStream.writeObject() traverses the entire object graph — the object and all reachable non-transient fields — and encodes them as a byte stream. ObjectInputStream.readObject() reconstructs the graph from those bytes, returning the root object which you cast to the expected type. Every class in the graph must also implement Serializable, or serialization throws a NotSerializableException.
import java.io.*;
import java.util.*;
// Serializable is a marker interface — no methods to implement
class Person implements Serializable {
private String name;
private int age;
private List<String> hobbies;
public Person(String name, int age, List<String> hobbies) {
this.name = name;
this.age = age;
this.hobbies = hobbies;
}
@Override public String toString() {
return name + " (" + age + ") " + hobbies;
}
}
// Serialize to a file
Person alice = new Person("Alice", 30, List.of("reading", "hiking"));
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("person.ser")))) {
oos.writeObject(alice);
System.out.println("Serialized: " + alice);
}
// Deserialize from the file
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(new FileInputStream("person.ser")))) {
Person restored = (Person) ois.readObject();
System.out.println("Deserialized: " + restored);
// Deserialized: Alice (30) [reading, hiking]
}
// ObjectOutputStream also handles primitive types and graphs
// oos.writeInt(42), oos.writeUTF("hello"), oos.writeObject(list)
// Serialize to byte array (useful for caching/transmission)
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(alice);
}
byte[] bytes = baos.toByteArray();
System.out.println("Byte size: " + bytes.length);
The transient keyword marks a field to be excluded from serialization. Use it for passwords, cached derived values, non-serializable objects like database connections, and anything that should not leave the JVM. After deserialization, transient fields are initialised to their default values (null, 0, false). serialVersionUID is a version stamp embedded in the serialized stream; during deserialization the JVM compares it to the local class's UID and throws InvalidClassException if they differ. Always declare it explicitly — without it, the JVM computes one automatically from the class structure, so any change to the class (adding a field, changing a method) silently breaks compatibility with previously serialized data.
class SecureUser implements Serializable {
// serialVersionUID — controls version compatibility
// If you change the class, increment this to force InvalidClassException on old data
// If omitted, JVM computes one — but any class change may break compatibility
private static final long serialVersionUID = 1L;
private String username;
private String email;
// transient — field is NOT serialized
// Use for: passwords, caches, derived data, non-serializable objects
private transient String password; // security — never serialize
private transient Connection dbConnection; // not serializable anyway
private transient int cachedHashCode; // can be recomputed
public SecureUser(String username, String email, String password) {
this.username = username;
this.email = email;
this.password = password;
}
// After deserialization, transient fields are their default values:
// null for objects, 0 for int, false for boolean
public String getPassword() { return password; } // will be null after deserialization
}
// Demonstrating serialVersionUID mismatch
// Changing the class after serializing data without updating serialVersionUID
// causes java.io.InvalidClassException: local class incompatible: ...
// stream classdesc serialVersionUID = 1, local class serialVersionUID = 2
// Best practice: always declare serialVersionUID explicitly
// Your IDE can generate it for you
Always declare serialVersionUID explicitly. Without it, any change to the class — adding a field, changing a method signature — can break deserialization of existing data with an InvalidClassException.
Declaring private void writeObject(ObjectOutputStream) and private void readObject(ObjectInputStream) in a Serializable class hooks into the serialization process without replacing it entirely. Call defaultWriteObject() or defaultReadObject() first to handle all non-transient fields automatically, then add custom logic: writing extra version tags, initialising transient fields from the deserialized data, or performing cross-field validation after reading. This is the primary mechanism for schema migration when a class evolves between serialized versions.
class Product implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private double price;
private transient String cachedDisplay; // derived, not serialized
public Product(String name, double price) {
this.name = name;
this.price = price;
this.cachedDisplay = name + " $" + price;
}
// Custom serialization — called by ObjectOutputStream
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // serialize all non-transient fields
// Write additional data if needed
oos.writeUTF("extra-metadata");
}
// Custom deserialization — called by ObjectInputStream
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject(); // deserialize all non-transient fields
String meta = ois.readUTF(); // read extra data
// Rebuild transient fields
this.cachedDisplay = name + " $" + price;
}
// readObjectNoData — called when class has no data in stream (version mismatch)
private void readObjectNoData() throws ObjectStreamException {
this.name = "Unknown";
this.price = 0.0;
}
}
// readResolve — control what object is actually returned during deserialization
// Useful for Singletons and enums
class Config implements Serializable {
private static final long serialVersionUID = 1L;
private static final Config INSTANCE = new Config();
private Config() {}
public static Config getInstance() { return INSTANCE; }
// Ensure singleton property after deserialization
private Object readResolve() {
return INSTANCE; // discard deserialized object, return the real singleton
}
}
Externalizable extends Serializable but requires you to implement writeExternal() and readExternal() explicitly. Nothing is serialized automatically — you write and read every field yourself, giving complete control over the wire format and the ability to omit fields, apply custom encoding, or achieve a more compact representation. The tradeoff is more boilerplate. One hard requirement: the class must have a public no-arg constructor, which is called during deserialization before readExternal() populates the fields.
import java.io.*;
// Externalizable — complete manual control; no automatic serialization
// Must implement writeExternal and readExternal
// Must have a public no-arg constructor (called during deserialization)
class Point3D implements Externalizable {
private double x, y, z;
public Point3D() {} // REQUIRED for Externalizable
public Point3D(double x, double y, double z) {
this.x = x; this.y = y; this.z = z;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeDouble(x);
out.writeDouble(y);
out.writeDouble(z);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
x = in.readDouble();
y = in.readDouble();
z = in.readDouble();
}
@Override public String toString() { return "(" + x + "," + y + "," + z + ")"; }
}
// Usage is the same as Serializable — use ObjectOutputStream/InputStream
Point3D p = new Point3D(1.0, 2.0, 3.0);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(p);
}
try (ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(baos.toByteArray()))) {
Point3D restored = (Point3D) ois.readObject();
System.out.println(restored); // (1.0,2.0,3.0)
}
// Externalizable is more efficient but requires more code
// Serializable is simpler; Externalizable gives full control over format
Deserializing data from untrusted sources is a well-known attack vector: carefully crafted byte streams can trigger arbitrary code execution during deserialization through chains of gadget classes already on the classpath. For data exchange with external systems, prefer formats like JSON or Protocol Buffers where deserialization does not execute application code. If Java serialization is unavoidable, use ObjectInputFilter (Java 9+) to whitelist the specific classes that are expected in the stream and reject everything else. Never deserialize input from the network, user-uploaded files, or any source you do not fully control without such a filter in place.
// SECURITY WARNING: Deserializing untrusted data is dangerous
// Attackers can craft byte streams that execute arbitrary code during deserialization
// This is one of the most critical Java security vulnerabilities
// Java 9+: ObjectInputFilter — allowlist/denylist deserialization
ObjectInputStream ois = new ObjectInputStream(inputStream);
// Set a filter that only allows Person class and standard types
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.Person;java.util.*;!*" // allow Person and java.util, deny rest
);
ois.setObjectInputFilter(filter);
// Global filter (Java 9+ system property or programmatic)
ObjectInputFilter.Config.setSerialFilter(filter);
// Best practices:
// 1. Never deserialize data from untrusted sources (network, files, user input)
// 2. Use allowlist filters for any deserialization
// 3. Consider alternatives to Java serialization:
// - JSON (Jackson, Gson) — human-readable, widely supported
// - Protocol Buffers — compact, schema-based, language-agnostic
// - MessagePack — compact binary, no code generation needed
// - Kryo — fast Java serialization library
// Enum serialization — enums are serialized by name, not structure
// Always safe — readResolve() is automatically used to return the enum constant
enum Status { ACTIVE, INACTIVE }
// Serializing Status.ACTIVE works correctly across class versions
// Records (Java 16+) are serializable if they implement Serializable
// Serialized form uses the canonical constructor — safer than class serialization
Never deserialize data from untrusted sources using Java's built-in serialization. Use ObjectInputFilter as a mitigation, but strongly prefer JSON or Protocol Buffers for data exchange with external systems.