Java Object Serialization: Serializable, transient, and serialVersionUID
Java object serialization converts an object graph to a byte stream that can be stored or transmitted, and deserialized back into objects. The Serializable marker interface enables this. Proper use requires declaring a serialVersionUID to control version compatibility, marking sensitive or non-serializable fields as transient, and understanding the security risks of deserializing untrusted data.
Contents
Basic Serialization
A class becomes serializable by implementing SerializableObjectOutputStream.writeObject()ObjectInputStream.readObject()SerializableNotSerializableException
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);
transient and serialVersionUID
The transientnull0falseserialVersionUIDInvalidClassException
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
serialVersionUIDInvalidClassExceptionCustom readObject and writeObject
Declaring private void writeObject(ObjectOutputStream)private void readObject(ObjectInputStream)SerializabledefaultWriteObject()defaultReadObject()
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: Full Control
ExternalizableSerializablewriteExternal()readExternal()publicreadExternal()
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
Security Considerations
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
// 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
ObjectInputFilter