Java Serialization and Deserialization with Serializable

Understand Java serialization with a working byte-stream example, Serializable, transient fields, serialVersionUID, compatibility errors, and JSON alternatives.

Last reviewed: November 2026

·Java 8Java 11Java 17Java 21
✅
Quick Understanding: Serialization converts object state into a byte stream, and deserialization reconstructs objects from that stream. Native Java serialization uses Serializable and object streams, couples stored data to Java classes, and should not accept untrusted input. JSON, Protobuf, and Avro provide other format choices for APIs and messages.

🔍 What Is It?

Java serialization writes an object's state to a byte stream; deserialization reconstructs an object from that stream. For ordinary classes, implement Serializable and use ObjectOutputStream.writeObject and ObjectInputStream.readObject. This native Java format is different from JSON serialization with Jackson.

Java Built-in Serialization:

  • →Implement java.io.Serializable (marker interface)
  • →Use ObjectOutputStream to serialize, ObjectInputStream to deserialize
  • →serialVersionUID — version ID for compatibility checking
  • →transient — exclude fields from serialization
  • →readObject/writeObject — customize serialization behavior

What gets serialized:

  • →All non-transient, non-static instance fields
  • →Recursively: all referenced objects (must also be Serializable)

Externalizable:

  • →More control: you implement readExternal/writeExternal
  • →Owns the stream format and its versioning; performance depends on the implementation
  • →Requires a public no-arg constructor

Modern Alternatives:

  • →JSON (Jackson, Gson) — human-readable, language-neutral, widely supported
  • →Protobuf — binary, fast, schema-based, language-neutral
  • →Avro — schema-based, great for Kafka
  • →Kryo — fast Java binary serialization (not language-neutral)
  • →Records — immutable data classes, serialize well with Jackson

🎯 Why Do We Need It?

Serialization is needed for:

  • →Persistence — save objects to disk (session state, caches)
  • →Network transmission — send objects over the network (RMI, Kafka, Redis)
  • →Caching — store objects in distributed caches (Redis, Hazelcast)
  • →Deep copy — serialize and deserialize to create a deep copy

However, Java's built-in serialization has serious problems: security vulnerabilities (deserialization attacks), tight coupling to class structure, poor performance. Use JSON or Protobuf instead.

🏗️ What Happens in Real Projects?

In enterprise Java:

  • →REST APIs — Jackson for JSON serialization/deserialization
  • →Kafka — Avro or JSON for message serialization
  • →Redis cache — JSON serialization via Spring Data Redis
  • →HTTP sessions — built-in serialization (if distributed session storage)
  • →JPA — not serialization; JPA maps objects to SQL directly

⚙️ How It Works

Built-in serialization process:

  1. ObjectOutputStream writes class descriptor (class name, serialVersionUID, field names/types)
  2. Writes field values recursively
  3. On deserialization: checks class compatibility and reconstructs the object. For ordinary Serializable classes, their constructors are skipped, but the first non-serializable superclass's accessible no-arg constructor runs. Externalizable classes and serializable records have different construction rules.

serialVersionUID:

If not declared, Java computes a UID from class details. Structural changes can change that value and prevent old streams from being read. Declare a stable UID for compatible evolution, then evaluate the actual change: adding a field can be compatible, while changing a field's type can be incompatible even with the same UID. Matching UIDs alone do not make every schema change safe.

Security issue:

Deserializing untrusted data can execute arbitrary code (gadget chains). Never deserialize data from untrusted sources with Java built-in serialization.

💻 Example

Serialization Patterns — When to use built-in serialization and modern alternatives.

java
import java.io.*;

public class SerializationDemo {
    private static class User implements Serializable {
        private static final long serialVersionUID = 1L;
        String name;
        transient String token;

        User(String name, String token) {
            this.name = name;
            this.token = token;
        }
    }

    public static void main(String[] args) throws Exception {
        byte[] bytes;
        try (ByteArrayOutputStream buffer = new ByteArrayOutputStream();
             ObjectOutputStream output = new ObjectOutputStream(buffer)) {
            output.writeObject(new User("Sam", "temporary-token"));
            output.flush();
            bytes = buffer.toByteArray();
        }

        // Read only the bytes created above, not data from an untrusted client.
        try (ObjectInputStream input = new ObjectInputStream(
                new ByteArrayInputStream(bytes))) {
            User restored = (User) input.readObject();
            System.out.println(restored.name);  // Sam
            System.out.println(restored.token); // null: transient field
        }
    }
}

🤔 Common Confusions

❓ Should I use Java built-in serialization or JSON?

Choose a format based on the system contract. JSON is widely used for REST APIs and cross-language messages; native Java serialization is mainly relevant to existing Java-only protocols or frameworks that require it. Neither format is automatically safe: validate data and avoid unsafe polymorphic deserialization. Benchmark performance for your actual payloads.

❓ What happens if I change a Serializable class?

An automatically computed serialVersionUID can change with class details, causing old streams to be rejected. Declare a stable UID for compatible changes and test old data. New fields absent from an old stream receive default values, not values from field initializers. Matching UIDs do not make incompatible field-type changes safe.

🚨 Common Production Issues

Deserialization of untrusted data — Java built-in deserialization can execute arbitrary code via gadget chains. Never deserialize untrusted data. Use ObjectInputFilter (Java 9+) to whitelist allowed classes.

serialVersionUID mismatch — a changed computed UID or an intentionally changed explicit UID can break old cached data. Keep the UID stable for compatible changes and plan migrations for incompatible changes.

Non-serializable fields causing NotSerializableException — all referenced objects must also be Serializable, or marked transient.

✅ Best Practices

Use Jackson/JSON instead of built-in serialization for new code.

Always declare serialVersionUID explicitly in Serializable classes.

Mark non-serializable fields as transient.

Never deserialize data from untrusted sources with built-in serialization.

Use @JsonIgnoreProperties(ignoreUnknown=true) for forward compatibility.

Configure ObjectMapper before use, then reuse it; avoid changing shared configuration after reads or writes begin.

🏛️ Architect's Note

Senior Architect's Perspective

Choose serialization formats as part of the service contract. JSON is widely supported and easy to inspect. Protobuf or Avro can provide compact messages and explicit schemas, but compare compatibility rules and benchmark representative payloads. For Kafka, Avro with a schema registry is one common option; choose according to the consumers and evolution requirements.

💬 Frequently Asked Questions

What is the difference between Serializable and Externalizable?

Serializable uses default field handling and can define customization hooks. Externalizable requires public readExternal and writeExternal methods and a public no-arg constructor; the class owns its external format. For ordinary Serializable classes, the first non-serializable superclass constructor runs. Serializable records use their canonical constructor. Performance depends on the implementation.

What is serialization and deserialization in Java?

Serialization writes object state into a byte stream using ObjectOutputStream. Deserialization reads that stream using ObjectInputStream to reconstruct objects. Classes participating in ordinary native object serialization implement Serializable.

What does transient do during serialization?

Default serialization skips transient instance fields. When an ordinary Serializable object is restored, a skipped reference field is null and a skipped primitive has its default value unless custom readObject logic restores it. Static fields belong to the class and are not serialized as object state.

What is serialVersionUID used for?

serialVersionUID identifies a serializable class version used during compatibility checking. Keep an explicit UID stable for compatible changes. Changing it intentionally prevents older streams from being read as that version, and keeping it unchanged does not override incompatible type changes.

Why do I get NotSerializableException?

An object reachable through a field is not serializable. Review the entire object graph, implement Serializable where appropriate, or exclude a field with transient and reconstruct it deliberately. Marking only the root object Serializable is not sufficient.

Why does deserialization throw InvalidClassException?

The local class and stream can have different serialVersionUID values, incompatible class changes, or another class-validity problem. Compare the exception message, the producer and reader class versions, and compatibility rules before deciding whether to migrate or discard stored data.

Is it safe to deserialize arbitrary client input?

Avoid native Java deserialization of untrusted input. Object input filters can restrict classes and graph size, but are an additional control rather than proof that arbitrary serialized data is safe. Prefer a deliberately constrained data format and validate its schema at system boundaries.

🎯 Key Takeaways

1Java built-in serialization: implements Serializable, uses ObjectOutputStream/InputStream
2Always declare serialVersionUID explicitly
3Mark non-serializable fields as transient
4Never deserialize untrusted data with built-in serialization (security risk)
5For new code, use Jackson JSON — safer, more maintainable, language-neutral
6Configure ObjectMapper before use and reuse it without later configuration changes

🎯 Practice this topic in interviews

↗ Where this topic fits

Sources

Sources / References

Use these Core Java references to verify the concept in official docs and continue into the most relevant SplashRide pages.

Related SplashRide pages