Custom Serialization Methods in Java

Private `writeObject` and `readObject` methods let a serializable class customize how its state is written and reconstructed. They are commonly used to control serialized fields, preserve invariants, or handle derived and transient state.

A class implementing `Serializable` can define private methods named `writeObject(ObjectOutputStream)` and `readObject(ObjectInputStream)`. Java's serialization mechanism recognizes these methods by their signatures and invokes them during default serialization and deserialization.

Inside `writeObject`, `defaultWriteObject()` can serialize the fields that normal serialization would handle, after which custom data can be written explicitly. `readObject` can call `defaultReadObject()` and then read the corresponding custom data in the same order and compatible format.

These methods are useful when an object's serialized representation should differ from simply storing every non-transient field. A common example is reconstructing a derived value, validating restored state, or serializing information that should not be held directly in a field.

`transient` fields are not serialized by default, so `readObject` can restore them from serialized information or recompute them. Because deserialization can create an object without executing its normal constructor, `readObject` is also an important place to enforce invariants and validate untrusted serialized state.

The method signatures matter: `writeObject` normally accepts `ObjectOutputStream` and `readObject` normally accepts `ObjectInputStream`, and both are private. Serialization is an object-stream protocol, so custom data written by `writeObject` must be consumed consistently by `readObject`.

Key points

Interview questions

What are `writeObject` and `readObject` used for?

They are private serialization hooks recognized by Java's built-in serialization mechanism. `writeObject` controls how an object's state is written, while `readObject` controls how that state is reconstructed. They can be combined with `defaultWriteObject()` and `defaultReadObject()` to retain normal serialization for ordinary fields. They are useful when default field-by-field serialization is insufficient.

Why would you call `defaultWriteObject()` inside `writeObject`?

It delegates the normal serialization of the class's serializable fields to the default mechanism. After that call, `writeObject` can write additional custom data or perform other serialization-specific work. The corresponding `readObject` should normally call `defaultReadObject()` to restore the default-managed state. This lets custom behavior coexist with ordinary field serialization.

How would you restore a `transient` field after deserialization?

A `transient` field is not restored by default serialization, so `readObject` must initialize it explicitly. Usually, the field is reconstructed from persistent fields or from custom data that `writeObject` placed in the stream. Calling `defaultReadObject()` first ensures the source fields have been restored before reconstruction. This is particularly useful for caches, derived values, or runtime-only resources.

What security concern should you consider when using Java deserialization?

Deserialization can execute class-specific reconstruction logic while turning attacker-controlled bytes into objects, so accepting untrusted serialized data can create serious security risks. A custom `readObject` method should not be treated as a safe boundary merely because it is private. Applications should avoid native Java serialization for untrusted inputs when possible and use appropriate filtering or safer data formats where applicable. Validation in `readObject` can protect object invariants, but it does not by itself make arbitrary deserialization safe.

What must be true about custom data written by `writeObject` and consumed by `readObject`?

The two methods must agree on the serialized representation and consume custom data compatibly. If `writeObject` writes a value, `readObject` must know how and when to read that value. Ordering matters because object streams are sequential. Changes to the format also have to be considered alongside serialization compatibility and `serialVersionUID`.