When designing a serializable type whose ancestors are not all serializable, what risks should you weigh and what alternatives exist?
answer
- Risks: lost state, stray ctor side effects, bypassed invariants, versioning
- Bypassed constructor checks = deserialization security hole
- Prefer DTO / JSON / Protobuf at trust boundaries
- If native: validate in readObject, readResolve, ObjectInputFilter
- Never deserialize untrusted input unfiltered
basics
~20 sRisks 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 sRelying 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
Aware that mixing serializable and non-serializable classes can lose data and that there are safer ways to move data around.
Lists the main risks (lost parent state, missing no-arg constructor) and knows JSON/DTOs are common alternatives.
Articulates constructor side effects, bypassed invariants as a security risk, and versioning fragility; can apply readObject validation and choose a DTO/JSON alternative.
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