RU | EN | DE

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 Serializable marker interface (without methods) allows the standard mechanism.
  • serialVersionUID is the class version. If not specified, the JVM will automatically generate it → even minor changes to the class structure can cause InvalidClassException. Manually fixing serialVersionUID stabilizes compatibility.
  • transient marks 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 serializedNotSerializableException. For DTOs – mapping (MapStruct), not direct serialization.
  • transient fields after deserialization will be null/0/false — don’t forget to initialize correctly.

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)

  1. Avoid standard Java serialization in public protocols and when working with untrusted data.
  2. Fix serialVersionUID.
  3. Mark sensitive data as transient or use DTOs without secrets.
  4. For fine logic – writeObject/readObject, readResolve/writeReplace, Serialization Proxy.
  5. For security – ObjectInputFilter (JEP 290), depth/byte/class limits.
  6. For inter-language interaction and stable versioning – Protobuf/Avro; for human-readable – JSON/Jackson.
  7. With JPA/LAZY – serialize DTOs, not entities.
  8. In distributed caches/queues – use supported serializers (Kafka Serde, Redis codecs, Spring MessageConverter).