Monday, March 30, 2026

Fundamental and superior Java serialization

non-public void readObject(ObjectInputStream in)  throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    this.rating = calculateScore();
}

Why order issues in customized serialization logic

When writing customized serialization logic, the order through which values are written should precisely match the order through which they’re learn:

non-public void writeObject(ObjectOutputStream out) throws IOException {
    out.defaultWriteObject();
    out.writeInt(42);
    out.writeUTF("Duke");
    out.writeLong(1_000_000L);
}
non-public void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    int stage = in.readInt();
    String title = in.readUTF();
    lengthy rating = in.readLong();
}

As a result of the stream is just not keyed by area title, every learn name merely consumes the following worth in sequence. If readUTF had been known as earlier than readInt, the stream would try to interpret the bytes of an integer as a UTF string, leading to corrupted knowledge or a deserialization failure. This is without doubt one of the primary causes customized serialization needs to be used sparingly. A helpful psychological mannequin is to consider serialization as a tape recorder: Deserialization should replay the tape in precisely the order it was recorded.

Why serialization is dangerous

Serialization is fragile when lessons change. Even small modifications could make beforehand saved knowledge unreadable.

Deserializing untrusted knowledge is especially harmful. Deserialization can set off sudden code paths on attacker‑managed object graphs, and this has been the supply of actual‑world safety vulnerabilities.

For these causes, Java serialization needs to be used solely in managed environments.

When serialization is sensible

Java serialization is appropriate just for a slim set of use circumstances the place class variations and belief boundaries are tightly managed.

Use case Advice
Inner caching Java serialization works properly when knowledge is short-lived and managed by the identical software.
Session storage Acceptable with care, supplied all collaborating techniques run suitable class variations.
Lengthy-term storage Dangerous: Even small class adjustments could make previous knowledge unreadable.
Public APIs Use JSON. It’s language-agnostic, secure throughout variations, and broadly supported. Java serialization exposes implementation particulars and is fragile.
System-to-system communication Want JSON or schema-based codecs resembling Protocol Buffers or Avro.
Cross-language communication Keep away from Java serialization totally. It’s Java-specific and never interoperable with different platforms.

Rule of thumb: If the info should survive class evolution, cross belief boundaries, or be consumed by non‑Java techniques, want JSON or a schema‑primarily based format over Java serialization.

Superior serialization methods

The mechanisms we’ve lined to this point deal with most sensible situations, however Java serialization has a couple of further instruments for fixing issues that default serialization can not.

Preserving singletons with readResolve

Deserialization creates a brand new object. For lessons that implement a single occasion, this breaks the assure silently:

public class GameConfig implements Serializable {

    non-public static last lengthy serialVersionUID = 1L;
    non-public static last GameConfig INSTANCE = new GameConfig();

    non-public GameConfig() {}

    public static GameConfig getInstance() {
        return INSTANCE;
    }

    non-public Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }
}

With out readResolve, deserializing a GameConfig would produce a second occasion, and any id verify utilizing == would fail. The tactic intercepts the deserialized object and substitutes the canonical one. The deserialized copy is discarded.

Substituting objects with writeReplace

Whereas readResolve controls what comes out of deserialization, writeReplace controls what goes into serialization. A category can outline this methodology to substitute a distinct object earlier than any bytes are written.

The 2 strategies are sometimes used collectively to implement a serialization proxy. One class represents the item’s runtime kind, whereas one other represents its serialized kind.

On this instance,ChallengerWriteReplace performs the function of the “actual” object, whereas ChallengerProxy represents its serialized kind:

public class ChallengerProxy implements Serializable {

    non-public static last lengthy serialVersionUID = 1L;

    non-public last lengthy id;
    non-public last String title;

    public ChallengerProxy(lengthy id, String title) {
        this.id = id;
        this.title = title;
    }

    non-public Object readResolve() throws ObjectStreamException {
        return new ChallengerWriteReplace(id, title);
    }
}

class ChallengerWriteReplace implements Serializable {

    non-public static last lengthy serialVersionUID = 1L;

    non-public lengthy id;
    non-public String title;

    public ChallengerWriteReplace(lengthy id, String title) {
        this.id = id;
        this.title = title;
    }

    non-public Object writeReplace() throws ObjectStreamException {
        return new ChallengerProxy(id, title);
    }
}

When a ChallengerWriteReplace occasion is serialized, its writeReplace methodology substitutes it with a light-weight ChallengerProxy. The proxy is the one object that’s truly written to the byte stream.

Throughout deserialization, the proxy’s readResolve methodology reconstructs a brand new ChallengerWriteReplace occasion, and the proxy itself is discarded. The applying by no means observes the proxy object straight.

This method retains the serialized kind decoupled from the inner construction of ChallengerWriteReplace. So long as the proxy stays secure, the primary class can evolve freely with out breaking beforehand serialized knowledge. It additionally offers a managed level the place invariants could be enforced throughout reconstruction.

Filtering deserialized lessons with ObjectInputFilter

I’ve defined why deserializing untrusted knowledge is harmful. Launched in Java 9, the ObjectInputFilter API offers functions a approach to prohibit which lessons are allowed throughout deserialization:

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
        "com.instance.mannequin.*;!*"
);

attempt (ObjectInputStream in = new ObjectInputStream(new FileInputStream("knowledge.ser"))) {
    in.setObjectInputFilter(filter); // should be set earlier than readObject()
    Object obj = in.readObject();
}

This filter permits solely lessons below com.instance.mannequin and rejects every thing else. The sample syntax helps allowlisting by package deal, in addition to setting limits on array sizes, object graph depth, and complete object rely.

Java 9 made it attainable to set a process-wide filter by way of ObjectInputFilter.Config.setSerialFilter or the jdk.serialFilter system property, making certain that no ObjectInputStream could be left unprotected by default. Java 17 prolonged this additional by introducing filter factories (ObjectInputFilter.Config.setSerialFilterFactory), which permit context‑particular filters to be utilized per stream fairly than counting on a single international coverage. In case your software deserializes knowledge that crosses a belief boundary, an enter filter is just not optionally available; it’s the minimal viable protection.

Java data and serialization

Java data can implement Serializable, however they behave in another way from unusual lessons in a single essential method: Throughout deserialization, the file’s canonical constructor is named. This implies any validation logic within the constructor runs on deserialized knowledge, which is a major security benefit:

public file ChallengerRecord(Lengthy id, String title) implements Serializable {
    public ChallengerRecord {
        if (id == null || title == null) {
            throw new IllegalArgumentException(
                    "id and title should not be null");
        }
    }
}

With a standard Serializable class, a corrupted or malicious stream may inject null values into fields that the constructor would usually reject. With a file, the constructor acts as a gatekeeper even throughout deserialization.

Data don’t help writeObject, readObject, or serialPersistentFields. Their serialized kind is derived totally from their elements, a design choice that deliberately favors predictability and security over customization.

Alternate options to Java serialization

The Externalizable interface is an alternative choice to Serializable that provides the category full management over the byte format. A category that implements Externalizable should outline writeExternal and readExternal, and should present a public no‑argument constructor:

public class ChallengerExt implements Externalizable {

    non-public lengthy id;
    non-public String title;

    public ChallengerExt() {} // required

    public ChallengerExt(lengthy id, String title) {
        this.id = id;
        this.title = title;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeLong(id);
        out.writeUTF(title);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException {
        this.id = in.readLong();
        this.title = in.readUTF();
    }
}

Not like Serializable, no area metadata or area values are written mechanically. The category descriptor (class title and serialVersionUID) continues to be written, however the developer is totally chargeable for writing and studying all occasion state.

As a result of writeExternal and readExternal work straight with primitives and uncooked values, fields ought to use primitive varieties the place attainable. Utilizing a wrapper kind resembling Lengthy with writeLong would throw a NullPointerException if the worth had been null, since auto‑unboxing can not deal with that case.

This strategy can produce extra compact output, however the developer is totally chargeable for versioning, area ordering, and backward compatibility.

In observe, Externalizable isn’t utilized in fashionable Java. When a full management over-the-wire format is required, most groups select Protocol Buffers, Avro, or related schema‑primarily based codecs as a substitute.

Conclusion

Java serialization is a low-level JVM mechanism for saving and restoring object state. Identified for being highly effective however unforgiving, serialization bypasses constructors, assumes secure class definitions, and offers no automated security ensures. Used intentionally in tightly managed techniques, it may be efficient. Used casually, it introduces delicate bugs and severe safety vulnerabilities. Understanding the trade-offs mentioned on this article will aid you use serialization appropriately and keep away from unintentional misuse.

Related Articles

Latest Articles