Serializing Records in Java
Records (see "Records in Java") can be serialized by adding implements Serializable, but Java treats them in a special, safer way. For a record, only the components are written. When it's read back, Java doesn't fill fields directly; it calls the record's canonical constructor with the saved values. That means any validation in the constructor runs again, so a tampered byte stream can't create an invalid record. In this lesson, you will see how record serialization works and why it is preferred.
How Records Are Serialized
| Normal class | Record | |
|---|---|---|
| What is written | All non-transient fields | The record components |
| How it is rebuilt | Fields set directly, no constructor | Canonical constructor is called |
| Validation in the constructor | Skipped | Runs again |
writeObject, readObject, readResolve hooks | Used | Ignored (except writeReplace and readResolve) |
serialVersionUID | Should be declared | Ignored; always 0L |
Example: Validation Runs on Deserialization
Let's see an example where a record checks its marks in a compact constructor.
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class RecordSerial {
record Student(String name, int marks) implements Serializable {
Student {
System.out.println("Constructor checks " + name + ", " + marks);
if (marks < 0 || marks > 100) {
throw new IllegalArgumentException("marks out of range");
}
}
}
public static void main(String[] args)
throws IOException, ClassNotFoundException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(bytes)) {
out.writeObject(new Student("Advitha", 85));
}
System.out.println("Written");
try (ObjectInputStream in = new ObjectInputStream(
new ByteArrayInputStream(bytes.toByteArray()))) {
Student s = (Student) in.readObject();
System.out.println("Read back: " + s);
}
}
}Constructor checks Advitha, 85
Written
Constructor checks Advitha, 85
Read back: Student[name=Advitha, marks=85]Explanation: The compact constructor ran twice: once when the record was created, and again during deserialization. If the bytes had been edited to say marks = 500, the second check would throw, and no invalid Student could ever be created. A normal class would skip its constructor and accept the bad value.
Prefer Records for Serialized Data
Common Mistakes
Adding writeObject or readObject to a record. They're ignored.
Declaring serialVersionUID and expecting it to be checked. Records always use 0L, and the check is skipped.
Assuming record components that are mutable objects are protected. A record with a List component still needs that list to be serializable, and its contents can still be changed.
Practice
- Easy: Make a
Point(int x, int y)record serializable and round-trip it. - Harder: Write a normal class and a record with the same validation (marks between 0 and 100). Explain, without tampering with bytes, why only the record is protected during deserialization.
Key Takeaways
Was this tutorial helpful?