skip to content

When designing a serializable type whose ancestors are not all serializable, what risks should you weigh and what alternatives exist?

level: principalimportance: nice to knowfreq 18%

answer

  1. Risks: lost state, stray ctor side effects, bypassed invariants, versioning
  2. Bypassed constructor checks = deserialization security hole
  3. Prefer DTO / JSON / Protobuf at trust boundaries
  4. If native: validate in readObject, readResolve, ObjectInputFilter
  5. Never deserialize untrusted input unfiltered

basics

~20 s

Risks include silently lost parent state, surprising constructor side effects firing on deserialization, skipped invariant checks (a security hole), and fragile cross-version compatibility. Safer alternatives are converting to a plain data-transfer object, using JSON or another explicit format, or making the whole hierarchy serializable if you own it.

solid answer

~40 s

Relying on Java serialization across a partly-non-serializable hierarchy carries several risks. Inherited state from non-serializable ancestors is silently lost (rebuilt by the no-arg constructor). The parent's no-arg constructor actually runs on deserialization, so its side effects (logging, ID generation, registry calls) fire unexpectedly, while serializable classes' constructors and their invariant checks are bypassed - meaning a crafted stream can yield an object in an illegal state, a known deserialization security risk. Cross-version evolution is brittle without disciplined serialVersionUID and serialized-form management. Given all this, prefer explicit alternatives for anything crossing a trust or persistence boundary: a hand-written serializable DTO, JSON/Protobuf with an explicit schema, or a static factory that validates. If you do use native serialization, validate in readObject, use readResolve, apply an ObjectInputFilter, and never deserialize untrusted input.

go deeper

for a junior

Aware that mixing serializable and non-serializable classes can lose data and that there are safer ways to move data around.

for a middle

Lists the main risks (lost parent state, missing no-arg constructor) and knows JSON/DTOs are common alternatives.

for a senior

Articulates constructor side effects, bypassed invariants as a security risk, and versioning fragility; can apply readObject validation and choose a DTO/JSON alternative.

for a principal

Sets policy: avoid native serialization at trust/persistence boundaries, mandate ObjectInputFilter and validation when unavoidable, weigh Externalizable vs schema formats, and own the long-term serialized-form contract.

## Why this is a design question, not just a mechanics question The mechanics (parent needs an accessible no-arg constructor; its fields aren't restored; its no-arg constructor runs; serializable classes' constructors are skipped) all combine into a set of **risks** that matter when you choose whether to use Java's built-in serialization at all. ## The risks, enumerated 1. **Silent state loss.** Fields declared in a non-serializable ancestor are never written and are reset by the ancestor's no-arg constructor on the way back. A round-trip silently changes object state — easy to miss until production data looks wrong. 2. **Surprising constructor side effects.** The non-serializable ancestor's no-arg constructor **runs** during deserialization. If it logs, allocates resources, registers the object in a global registry, increments counters, or contacts a service, those effects fire every time you deserialize — often unexpectedly and possibly multiple times. 3. **Bypassed invariants = security risk.** Constructors of the **serializable** classes are **skipped**, so any validation/invariant enforcement you placed there does not run. Java's `ObjectInputStream` reconstructs objects without your constructor, so a malicious or corrupted byte stream can fabricate objects in states your constructors would forbid (negative balances, broken collections, gadget chains). This is the root of the well-known Java deserialization vulnerability class. 4. **Fragile versioning.** A serializable class's fields are part of its **serialized form** — a long-lived contract. Without an explicit `serialVersionUID` and careful field management (`transient`, custom `writeObject`/`readObject`, `Externalizable`), evolving the class (or its serializable ancestors) breaks compatibility with previously serialized bytes (`InvalidClassException`). Mixing serializable and non-serializable layers makes this reasoning harder. 5. **Hidden coupling.** Preserving non-serializable-parent state forces the subclass to manually read/write parent fields, duplicating knowledge of the parent's internals and breaking whenever the parent changes. ## Alternatives, from most to least preferred - **Explicit DTO + mapping.** Define a small, purpose-built serializable (or JSON-mapped) data-transfer object containing exactly the fields you need; map to/from your domain objects. No hierarchy surprises, no hidden side effects, validated on construction. - **Schema-based formats (JSON, Protobuf, Avro).** Use Jackson/Gson/Protobuf with an explicit schema and versioning story. Human-inspectable, language-neutral, far safer for untrusted or persisted data. - **Make the hierarchy fully serializable (if you own it)** and treat it as a deliberate contract: explicit serialVersionUID, `transient` for non-persistent fields, validation in `readObject`. - **`Externalizable`** when you want full manual control of the byte layout (you implement `writeExternal`/`readExternal`); more control, more responsibility, and it DOES call a public no-arg constructor. ## If you must use native serialization - **Validate in `readObject`:** call `defaultReadObject()`, then check invariants and throw `InvalidObjectException` on violation. - **`readResolve`/`writeReplace`:** substitute a canonical or validated instance (also enforces singletons/enums). - **`ObjectInputFilter` (JDK 9+):** whitelist allowed classes and limit depth/size to blunt gadget-chain and resource-exhaustion attacks. - **Never deserialize untrusted input** with the built-in mechanism without a strict filter; prefer a data format instead. ## Bottom line A serializable type over a partly-non-serializable hierarchy works, but it concentrates several footguns: lost state, stray constructor side effects, bypassed validation (a security hole), and brittle versioning. For boundaries that cross trust or persistence, prefer explicit DTOs/JSON with validation; reserve native serialization for controlled, in-process, fully-owned cases — and even then harden it.

  • Name two concrete mitigations if you must deserialize with Java's built-in mechanism.
    Validate invariants in readObject (call defaultReadObject then check, throwing InvalidObjectException), and install an ObjectInputFilter (JDK 9+) to whitelist classes and cap depth/size; also avoid deserializing untrusted input at all.
  • How does Externalizable differ from the default Serializable handling regarding constructors?
    Externalizable requires a public no-arg constructor that IS called during deserialization, after which readExternal repopulates the fields you choose - unlike default Serializable, which skips constructors of serializable classes and reconstructs fields automatically.

saying these in an interview costs you the question

  • Treating Java serialization as safe for untrusted input
  • Assuming constructor validation still protects deserialized objects
  • Ignoring constructor side effects that fire on deserialize
  • Skipping serialVersionUID and a versioning plan
  • Reaching for native serialization when a DTO/JSON would be safer

context