Serialization in Java converts supported object state into a byte stream. Deserialization reads that representation and reconstructs objects. ObjectOutputStream and ObjectInputStream implement Java's native object format, while Serializable marks ordinary classes that participate in it.
This format is closely tied to Java classes and their compatibility rules. It is different from converting an object to JSON. Learn it for controlled Java-specific storage or existing systems, but prefer an explicit data format when applications need transparent, cross-language exchange.
What Serializable actually promises
Serializable is a marker interface: it declares no methods that your class must implement. By default, ordinary serialization records its nonstatic, nontransient instance fields and follows their referenced objects.
Every relevant referenced object must also support serialization, unless the reference is excluded or handled through a deliberate custom mechanism. Marking only the outer class is not enough when it contains a nonserializable component. A failure can therefore occur inside the object graph rather than at its root.
Static fields belong to the class, so they are not part of an individual object's saved state. Methods are not copied into the stream either. The receiving environment needs compatible class definitions to interpret the saved data.
Object identity within one serialized graph is tracked. If two fields reference the same object, deserializing that graph normally preserves their shared reference relationship. This is not equivalent to independently copying every field into unrelated objects.
serialVersionUID and evolving classes
serialVersionUID identifies a serialization version for a class. When an input stream's identifier disagrees with the local class definition, deserialization commonly fails with InvalidClassException. Explicitly declaring it avoids relying on a generated value that can change after incidental class edits.
Keeping the same identifier is not proof that every class change is compatible. Changing a field's type, removing expected behavior or altering an invariant can still make old data unusable or misleading. Treat stored objects as a data contract that needs migration decisions.
For ordinary Serializable classes, deserialization does not run their own normal constructors in the same way as new. The first nonserializable superclass constructor participates instead. Records have different reconstruction rules involving their canonical constructor. Do not assume a constructor's validation automatically protects every deserialized ordinary object.
When durable data must survive many application versions, a documented external schema is often easier to evolve than binding storage directly to implementation classes.
transient is not encryption
A transient field is omitted from default serialization. That does not encrypt other fields, erase memory, or protect a secret saved through another reference. Never rely on the keyword as a general credential-storage mechanism.
Use transient for state that should be recomputed or not persisted, such as a temporary cache. If that state is required before methods can run, make its restoration explicit. A deserialized cache reference left null can cause failures long after reading succeeds.
Custom writeObject and readObject methods can change default behavior, but they introduce another compatibility and validation contract. Learn the ordinary form first. A small schema-based DTO is often simpler than complex hooks that reproduce an application's whole runtime state.
Choosing a format and validating results
Avoid native deserialization of untrusted input. Allowed classes may have custom behavior, so their code and invariants matter too. Oracle's serialization filtering guide explains class restrictions and graph limits.
After reading controlled data, check the expected type and meaningful values before using them. Negative seat numbers or invalid event names can remain invalid even when the class is recognized.
Do not append a new ObjectOutputStream header to an existing native object file without a format plan. A normal object stream begins with its own header; repeated headers can break an ordinary reader. For simple exchange, JSON with explicit fields and validation is usually easier to inspect.
Keep round-trip tests, older compatible fixtures and intentionally rejected data separate. Successful loading of one current object does not establish future compatibility or a safe external-input policy.
Serialization and Deserialization Summary
- Save controlled state rather than an entire live application.
- Treat version identifiers and data evolution as separate decisions.
- transient excludes default state but does not encrypt it.
- Restrict types and resources before reading supported native data.
- Compare the explicit format in JSON processing.
Serialization and Deserialization Examples
Each example shows the code, the output it produces and what each part does.
An object round trip in memory
Save this complete program as RecitalSnapshotExample.java.
The examples serialize only objects created within these programs. A byte array is convenient for a fixture; file-backed object streams follow the same format but add file ownership and failure handling.
import java.io.*;
public class RecitalSnapshotExample {
static class Seat implements Serializable {
private static final long serialVersionUID = 1L;
String event;
int number;
Seat(String event, int number) {
this.event = event;
this.number = number;
}
}
public static void main(String[] args) throws Exception {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream output = new ObjectOutputStream(bytes)) {
output.writeObject(new Seat("Evening recital", 14));
}
try (ObjectInputStream input = new ObjectInputStream(
new ByteArrayInputStream(bytes.toByteArray()))) {
Seat restored = (Seat) input.readObject();
System.out.println(restored.event);
System.out.println(restored.number);
}
}
}Evening recital
14Line-by-line explanation
- Lines 4 to 5The class opts into serialization and declares an explicit compatibility identifier.
- Lines 16 to 17The output stream records the seat's fields in an isolated byte destination.
- Lines 19 to 21The input stream reconstructs an object, and the caller casts it to the expected type.
- Lines 22 to 23Both saved values are available after the round trip.
Excluding temporary state with transient
A temporary preview selection should not become part of a saved workspace.
The initializer assigning Expanded runs during ordinary construction of the source object. It does not restore that value during this ordinary-class deserialization. Reinitialize temporary state through a deliberate reconstruction strategy when the application needs it.
import java.io.*;
public class WorkspaceStateExample {
static class Workspace implements Serializable {
private static final long serialVersionUID = 1L;
String name = "Sketches";
transient String preview = "Expanded";
}
public static void main(String[] args) throws Exception {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream output = new ObjectOutputStream(bytes)) {
output.writeObject(new Workspace());
}
try (ObjectInputStream input = new ObjectInputStream(
new ByteArrayInputStream(bytes.toByteArray()))) {
Workspace restored = (Workspace) input.readObject();
System.out.println(restored.name);
System.out.println(restored.preview);
}
}
}Sketches
nullLine-by-line explanation
- Lines 6 to 7Name is persistent state; preview is excluded from the default saved form.
- Lines 12 to 13Writing the workspace records name but omits preview.
- Lines 17 to 19The reconstructed transient reference has its default null value.
Rejecting an unexpected serialized class
This example writes an Integer locally, then demonstrates a filter that permits a different application type.
A filter is an additional boundary, not permission to accept arbitrary serialized uploads. Concrete String values and primitives are not checked like ordinary object classes. Apply input-size limits and value validation as well; this deliberately small filter does not claim to secure every data contract.
import java.io.*;
public class SerializationFilterExample {
static class Note implements Serializable {
private static final long serialVersionUID = 1L;
int pages;
}
public static void main(String[] args) throws Exception {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream output = new ObjectOutputStream(bytes)) {
output.writeObject(Integer.valueOf(7));
}
try (ObjectInputStream input = new ObjectInputStream(
new ByteArrayInputStream(bytes.toByteArray()))) {
input.setObjectInputFilter(info -> {
if (info.depth() > 3 || info.references() > 20) {
return ObjectInputFilter.Status.REJECTED;
}
Class<?> type = info.serialClass();
if (type == null) return ObjectInputFilter.Status.UNDECIDED;
return type == Note.class
? ObjectInputFilter.Status.ALLOWED
: ObjectInputFilter.Status.REJECTED;
});
input.readObject();
} catch (InvalidClassException error) {
System.out.println("Unexpected class rejected");
}
}
}Unexpected class rejectedLine-by-line explanation
- Line 12The fixture contains an Integer, not the expected Note type.
- Lines 16 to 18The stream filter rejects excessive graph depth or reference count.
- Lines 20 to 24A class check permits Note and rejects other presented classes.
- Lines 27 to 28Reading the disallowed type reports rejection as InvalidClassException.
Serialization and Deserialization FAQ
Is Java serialization the same as JSON?
No. Native serialization encodes Java object graphs and class compatibility information. JSON expresses a language-independent data structure.
Does readObject return a fully validated domain object?
Not automatically. Enforce the accepted type, field invariants and input policy for your application.
Can records be serialized?
Yes, if the record implements Serializable. Records are deserialized through their canonical constructor, so any validation in a compact constructor runs on the incoming data too. That makes records safer to deserialize than ordinary classes, whose fields are filled in without calling a constructor.
Why is deserializing untrusted data dangerous?
The serialized bytes decide which classes are created and how their fields are set, and some classes run code while being restored. Attackers have used chains of such classes to run commands on servers. Never deserialize data from untrusted sources with ObjectInputStream, or if you must, restrict the allowed classes with an ObjectInputFilter.
What should I use instead of Java serialization?
For data exchanged with other programs or stored for a long time, use a documented format such as JSON with Jackson, Protocol Buffers, or a database. These formats don't depend on Java class internals, are easier to evolve and don't create objects of arbitrary classes from input.
Serialization and Deserialization Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Which field is omitted by default serialization?
What happens to Workspace.preview after the shown round trip?
Serialization and Deserialization Quiz
2 questions to check what you learned.
Does declaring the same serialVersionUID prove every class edit is safe?
- No
- Yes, regardless of field types
- Only if the class name is short
Why does the filter example reject Integer?
- It is not the permitted Note class
- Seven cannot be stored in an Integer
- ObjectOutputStream never writes numbers