What is it and why?
Serialization is the process of converting an object into a stream of bytes (for file writing/network transmission/caching). Deserialization is the reverse process of restoring an object from bytes.
Where it is used:
- Distributed systems (RPC, REST, Kafka), sessions (HttpSession), caches (Redis, Hazelcast), queues (JMS), state snapshots.
Basic Serialization: Serializable
Minimal Example
import java.io.*;
public class User implements Serializable {
private static final long serialVersionUID = 1L; // fix the class version
private String username;
private transient String password; // not serialized
public User(String username, String password) {
this.username = username;
this.password = password;
}
}
// write
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
out.writeObject(new User("alice", "p@ss"));
}
// read
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
User u = (User) in.readObject();
}Key Points
- The
Serializablemarker interface (without methods) allows the standard mechanism. serialVersionUIDis the class version. If not specified, the JVM will automatically generate it → even minor changes to the class structure can causeInvalidClassException. Manually fixingserialVersionUIDstabilizes compatibility.transientmarks fields that should not be included in the stream (passwords, caches, dependencies, lazy-proxies, etc.).
Customizing the Process
1. writeObject/readObject
Allows controlling serialization/deserialization.
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // standard logic
// additionally encrypt the password and write it
String enc = password == null ? null : encrypt(password);
out.writeObject(enc);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // standard logic
String enc = (String) in.readObject();
this.password = enc == null ? null : decrypt(enc);
}2. readResolve / writeReplace
readResolve()returns a replacement object after deserialization (useful for singletons/caches).writeReplace()replaces the object before serialization (Serialization Proxy pattern).
// Singleton
public class AppConfig implements Serializable {
private static final AppConfig INSTANCE = new AppConfig();
private AppConfig() {}
public static AppConfig getInstance() { return INSTANCE; }
private Object readResolve() { return INSTANCE; } // preserve singleton guarantee
}3. serialPersistentFields
Defines the “logical” set of serializable fields, regardless of the actual ones.
private static final ObjectStreamField[] serialPersistentFields = {
new ObjectStreamField("username", String.class)
};Full Control: Externalizable
If you need full manual serialization:
import java.io.*;
public class Point implements Externalizable {
private int x, y;
public Point() {} // mandatory public constructor without arguments
public Point(int x, int y) { this.x = x; this.y = y; }
@Override public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(x); out.writeInt(y);
}
@Override public void readExternal(ObjectInput in) throws IOException {
this.x = in.readInt(); this.y = in.readInt();
}
}Pros: format control, speed, compatibility. Cons: more code, higher risk of errors.
Security: Filtering and Restrictions (JEP 290)
Problem: Java serialization can lead to vulnerabilities (gadget-chains, RCE) when deserializing untrusted data.
Solutions:
- ObjectInputFilter (JDK 9+): whitelist/blacklist classes, graph limits.
import java.io.*;
import java.util.Objects;
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"maxdepth=50;maxbytes=1048576;!*;java.base/*;com.myapp.*"
);
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("in.bin"))) {
in.setObjectInputFilter(filter);
Object obj = in.readObject();
}- Never deserialize data from external sources using standard Java serialization.
- For network protocols – alternative formats (see below).
Version Compatibility (Versioning)
What breaks compatibility:
- deleted/changed field type;
- changed hierarchy (parent);
- renamed class/package;
- no suitable constructor (for
Externalizable). Strategies: - fix
serialVersionUID; - for new fields – provide default values in
readObject; - use Serialization Proxy: serialize a simple “DTO proxy” instead of the complex entity;
- if possible – switch to protocols with explicit schema (Protobuf/Avro).
Performance and Pitfalls
- Slower and noisier for GC than binary specialized formats (Protobuf/Kryo).
- Poorly portable between languages (Java-specific).
- Cyclic object graphs are supported, but can explode in size.
- JPA entities with LAZY-fields: library proxy objects often are not serialized →
NotSerializableException. For DTOs – mapping (MapStruct), not direct serialization. transientfields after deserialization will benull/0/false — don’t forget to initialize correctly.
Alternatives (Recommended in Production)
1. JSON (often Jackson)
Pros: readable, cross-language, easy to debug. Cons: verbose, slower than binary formats.
// Jackson
import com.fasterxml.jackson.databind.ObjectMapper;
ObjectMapper om = new ObjectMapper();
String json = om.writeValueAsString(new User("alice", "p@ss")); // password can be @JsonIgnore
User u = om.readValue(json, User.class);Annotations: @JsonProperty, @JsonIgnore, @JsonCreator, @JsonTypeInfo (polymorphism), @JsonInclude.
2. Protocol Buffers (Google Protobuf)
Pros: fast, compact, strict schema with versioning, excellent cross-language support. Cons: binary (not human-readable), requires .proto compilation.
// user.proto
syntax = "proto3";
package demo;
message User {
string username = 1;
string email = 2;
}Generate Java classes (protoc), then:
UserProto.User u = UserProto.User.newBuilder()
.setUsername("alice").setEmail("a@x.io").build();
byte[] bytes = u.toByteArray();
UserProto.User restored = UserProto.User.parseFrom(bytes);3. Avro
Pros: dynamic/evolutionary schema (often in Kafka), compression, binary format, good for event logging. Cons: more complex setup than JSON.
4. Kryo
Pros: very fast, compact, can serialize arbitrary graphs. Cons: sensitive to class changes, requires registration control, more suitable for in-JVM/caches/Spark, than public APIs.
Best Practices (Summary)
- Avoid standard Java serialization in public protocols and when working with untrusted data.
- Fix
serialVersionUID. - Mark sensitive data as
transientor use DTOs without secrets. - For fine logic –
writeObject/readObject,readResolve/writeReplace, Serialization Proxy. - For security – ObjectInputFilter (JEP 290), depth/byte/class limits.
- For inter-language interaction and stable versioning – Protobuf/Avro; for human-readable – JSON/Jackson.
- With JPA/LAZY – serialize DTOs, not entities.
- In distributed caches/queues – use supported serializers (Kafka Serde, Redis codecs, Spring MessageConverter).