Java Serialization and Deserialization Basics
Introduction
Serialization turns a Java object - together with all the objects it refers to - into a sequence of bytes, which can be saved to a file or sent over a network. Deserialization turns those bytes back into objects. Java has had a built-in mechanism for this since version 1.1. This tutorial explains the basics of Java serialization and deserialization. We will start by making a class
(Note - This tutorial uses the byte streams from Byte Streams vs Character StreamsClick to read tutorial: Java Byte Streams vs Character Streams and Buffered I/O.) Making a class serializable A class opts in to serialization by implementing
OUTPUT of the above code
Explanation of the code
Non-serializable fields and shared objects
If any object in the graph is not serializable,
OUTPUT of the above code
The two people still share a single
serialVersionUID - versioning a class
Serialized bytes may be read back long after they were written, by a newer version of the class. Every serializable class has a version number,
If you do not declare one, Java calculates it from the class's details - fields, methods, interfaces. Almost any change to the class, even adding a method, then changes the number and makes all previously saved data unreadable. So always declare it explicitly, as
OUTPUT of the above code
The computed numbers are long hash values, worked out from the class name, its interfaces, its fields and its non-private methods and constructors. A class cannot be compiled twice in one program, so the example compares two similar classes; in a real project, adding the
OUTPUT of the above code
Security risks - and modern alternatives
Java serialization has a serious weakness: deserializing untrusted data is dangerous.
Serializable, and saving and restoring an object. We will then leave fields out with transient, and see what happens to static fields and to objects shared between references. Next we will understand serialVersionUID and what happens when a class changes. Lastly, we will look at serializing records, and at the security risks which mean modern applications usually prefer formats such as JSON.(Note - This tutorial uses the byte streams from Byte Streams vs Character StreamsClick to read tutorial: Java Byte Streams vs Character Streams and Buffered I/O.) Making a class serializable A class opts in to serialization by implementing
java.io.Serializable - a marker interface with no methods. Objects are written - inside try-with-resourcesClick to read tutorial: Java try-with-resources and AutoCloseable Explained - with ObjectOutputStream.writeObject() and read back with ObjectInputStream.readObject(), which returns an Object that must be cast.
A serializable class
//Employee.java
import java.io.Serializable;
import java.util.List;
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private final String name;
private final double salary;
private final List<String> skills;
private transient String sessionToken; // not serialized
static int instancesCreated = 0; // static: belongs to the class, not serialized
public Employee(String name, double salary, List<String> skills, String sessionToken) {
this.name = name;
this.salary = salary;
this.skills = skills;
this.sessionToken = sessionToken;
instancesCreated++;
}
@Override
public String toString() {
return "Employee[name=" + name + ", salary=" + salary + ", skills=" + skills + ", sessionToken=" + sessionToken + "]";
}
}
Serializing an object to a file and reading it back
//SaveAndLoad.java
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public class SaveAndLoad {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Path file = Path.of("employee.ser");
Employee tom = new Employee("Tom", 85000, new ArrayList<>(List.of("Java", "SQL")), "a1b2c3");
System.out.println("Original : " + tom);
try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(file))) {
out.writeObject(tom);
}
System.out.println("Written : " + Files.size(file) + " bytes");
try (ObjectInputStream in = new ObjectInputStream(Files.newInputStream(file))) {
Employee restored = (Employee) in.readObject();
System.out.println("Restored : " + restored);
System.out.println("Same object? " + (restored == tom) + ", instancesCreated = " + Employee.instancesCreated);
}
Files.delete(file);
}
}
Original : Employee[name=Tom, salary=85000.0, skills=[Java, SQL], sessionToken=a1b2c3] Written : 175 bytes Restored : Employee[name=Tom, salary=85000.0, skills=[Java, SQL], sessionToken=null] Same object? false, instancesCreated = 1
- The whole object graph was saved - the
Employee, itsStringname and itsArrayListof skills - and rebuilt. Every object in the graph must be serializable;StringandArrayListare. - The
transientfieldsessionTokenwas skipped, so it came back with its default value,null. Usetransientfor secrets, caches and values which make no sense outside the running program. - Deserialization created a new object, without calling the constructor -
instancesCreatedstayed at 1. Static fields belong to the class, so they are never serialized. readObject()can throwClassNotFoundExceptionif the class of a stored object is not available when reading.
writeObject() fails with a NotSerializableException. And when two references point to the same object, serialization stores that object once, so the shared structure survives the round trip.
Shared references and a non-serializable field
//GraphDemo.java
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class GraphDemo {
static class Address implements Serializable {
String city;
Address(String city) { this.city = city; }
}
static class Person implements Serializable {
String name;
Address home;
Person(String name, Address home) { this.name = name; this.home = home; }
}
static class Logger { } // not Serializable
static class Service implements Serializable {
Logger logger = new Logger();
}
static byte[] serialize(Object o) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(bytes)) {
out.writeObject(o);
}
return bytes.toByteArray();
}
public static void main(String[] args) throws Exception {
Address shared = new Address("London");
Person[] couple = {new Person("Tom", shared), new Person("Nancy", shared)};
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(serialize(couple)))) {
Person[] copy = (Person[]) in.readObject();
System.out.println("Still one shared Address? " + (copy[0].home == copy[1].home));
}
try {
serialize(new Service());
} catch (NotSerializableException e) {
System.out.println("NotSerializableException: " + e.getMessage());
}
}
}
Still one shared Address? true NotSerializableException: GraphDemo$Logger
Address after deserialization. The Service could not be serialized because its Logger field is not serializable - marking that field transient would fix it.
serialVersionUID; when reading, Java compares the number stored in the bytes with the class's current number, and refuses to load the object if they differ.If you do not declare one, Java calculates it from the class's details - fields, methods, interfaces. Almost any change to the class, even adding a method, then changes the number and makes all previously saved data unreadable. So always declare it explicitly, as
Employee does with private static final long serialVersionUID = 1L;. With a fixed number, compatible changes - such as adding a field, which simply gets its default value when old data is read - keep working, and you change the number deliberately only when old data can no longer be read correctly.
Viewing the serialVersionUID of classes
//VersionCheck.java
import java.io.ObjectStreamClass;
import java.io.Serializable;
public class VersionCheck {
static class WithoutUid implements Serializable {
String name;
}
static class WithoutUidPlusMethod implements Serializable {
String name;
String greet() { return "hi " + name; } // the only difference: one extra method
}
public static void main(String[] args) {
System.out.println("Employee (declared) : " + ObjectStreamClass.lookup(Employee.class).getSerialVersionUID());
System.out.println("computed, class A : " + ObjectStreamClass.lookup(WithoutUid.class).getSerialVersionUID());
System.out.println("computed, A + method : " + ObjectStreamClass.lookup(WithoutUidPlusMethod.class).getSerialVersionUID());
}
}
Employee (declared) : 1 computed, class A : 2571512725159962110 computed, A + method : -4108601833121539039
greet() method to a class with no declared number would likewise change its computed number, and reading data saved by the old version would fail with an InvalidClassException. The JDK's serialver tool prints the computed number of an existing class, useful when adding an explicit one to a class which already has saved data.
Serializing records
Records (Java 16+) can be serialized too, and are the safest kind of serializable class: they are always deserialized through their canonical constructor, so any validation in it runs again for data read from a file.
A serializable record is rebuilt through its constructor
//RecordSerialization.java
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class RecordSerialization {
record Point(int x, int y) implements Serializable {
Point {
System.out.println(" constructor called with (" + x + ", " + y + ")");
}
}
public static void main(String[] args) throws Exception {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(bytes)) {
out.writeObject(new Point(3, 4));
}
System.out.println("Serialized; now reading back:");
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
System.out.println("Restored: " + in.readObject());
}
}
}
constructor called with (3, 4) Serialized; now reading back: constructor called with (3, 4) Restored: Point[x=3, y=4]
readObject() can create objects of any serializable class on the classpath and run code in them, and attackers have used carefully crafted byte streams ("gadget chains") to run arbitrary code on servers. Joshua Bloch's advice in Effective Java is that there is no reason to use Java serialization in new systems.- Never deserialize data from untrusted sources - network requests, uploaded files, cookies.
- If you must, restrict which classes can be read with a serialization filter (
ObjectInputFilter, Java 9+, or thejdk.serialFiltersetting). - Prefer data formats - JSON (with libraries such as Jackson or Gson), XML, or Protocol Buffers. They are readable by other languages, easy to inspect, and do not execute code when read.
Serializable, and saved and restored a whole object graph with ObjectOutputStream and ObjectInputStream. We saw that transient and static fields are not serialized, and that deserialization does not call the constructor. We then saw that shared objects stay shared, and that a non-serializable field causes a NotSerializableException. Next we learned why serialVersionUID should always be declared. We then serialized a record, whose constructor runs on deserialization, and lastly looked at the security risks of deserialization and at modern alternatives such as JSON. This completes the file I/O module. Next, we move on to concurrency with synchronized and volatileClick to read tutorial: Java synchronized and volatile Explained with Examples.Core Java Tutorials
Java Byte Streams vs Character Streams and Buffered I/OClick to read tutorial: Java Byte Streams vs Character Streams and Buffered I/O Java synchronized and volatile Explained with ExamplesClick to read tutorial: Java synchronized and volatile Explained with Examples Reading and Writing Files in Java with Path and FilesClick to read tutorial: Reading and Writing Files in Java with Path and Files Stack vs Heap Memory in Java and Object ReferencesClick to read tutorial: Stack vs Heap Memory in Java and Object References
Java Byte Streams vs Character Streams and Buffered I/OClick to read tutorial: Java Byte Streams vs Character Streams and Buffered I/O Java synchronized and volatile Explained with ExamplesClick to read tutorial: Java synchronized and volatile Explained with Examples Reading and Writing Files in Java with Path and FilesClick to read tutorial: Reading and Writing Files in Java with Path and Files Stack vs Heap Memory in Java and Object ReferencesClick to read tutorial: Stack vs Heap Memory in Java and Object References