Serializing Records in Java

Naveen AnamaneniUpdated · 3 min read

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 classRecord
What is writtenAll non-transient fieldsThe record components
How it is rebuiltFields set directly, no constructorCanonical constructor is called
Validation in the constructorSkippedRuns again
writeObject, readObject, readResolve hooksUsedIgnored (except writeReplace and readResolve)
serialVersionUIDShould be declaredIgnored; always 0L

Example: Validation Runs on Deserialization

Let's see an example where a record checks its marks in a compact constructor.

RecordSerial.java▶ Try it
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);
        }
    }
}
Output
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

  1. Easy: Make a Point(int x, int y) record serializable and round-trip it.
  2. 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.

Quick check: Serializing records

4 questions
1What is written when a record is serialized?
2How is a record rebuilt during deserialization?
3Is serialVersionUID checked for records?
4Why is record serialization safer?

Key Takeaways

Was this tutorial helpful?

Lesson 375 of 482