00:00

Serialization and Deserialization in Java

Serialization converts an object, and everything it references, into a stream of bytes that can be saved to a file, stored in a cache or sent over a network. Deserialization rebuilds an equivalent object from those bytes, possibly in another JVM. Java's built-in mechanism is enabled by implementing java.io.Serializable and uses ObjectOutputStream and ObjectInputStream.

This tutorial covers the basics, serialVersionUID, transient fields, inheritance rules, custom serialization, records, Externalizable, and the security concerns that make modern applications prefer formats such as JSON.

1. How Serialization Works

Serialization converts an object to bytes; deserialization rebuilds it
  1. The class implements Serializable, a marker interface with no methods.
  2. ObjectOutputStream.writeObject() writes the object's class description and the values of its non-static, non-transient fields, recursively following references (the object graph).
  3. ObjectInputStream.readObject() reads them back and creates a new object without calling its constructor.

2. Serializing to a File and Back

SerializeToFile.java
import java.io.*;
import java.nio.file.*;
import java.util.List;

public class SerializeToFile {
    static class Customer implements Serializable {
        private static final long serialVersionUID = 1L;    // version of the serialized form
        private final String name;
        private final int loyaltyPoints;
        private final List<String> orders;
        private transient String sessionToken;              // not serialized

        Customer(String name, int points, List<String> orders, String token) {
            this.name = name; this.loyaltyPoints = points; this.orders = orders; this.sessionToken = token;
        }
        public String toString() {
            return "Customer[" + name + ", points=" + loyaltyPoints + ", orders=" + orders + ", token=" + sessionToken + "]";
        }
    }

    public static void main(String[] args) throws Exception {
        Path file = Path.of(System.getProperty("java.io.tmpdir"), "customer.ser");
        Customer original = new Customer("Asha", 1250, List.of("A-17", "B-42"), "tok-9f2c");

        try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(file))) {
            out.writeObject(original);                       // serialization
        }
        System.out.println("Saved to " + file.getFileName());

        try (ObjectInputStream in = new ObjectInputStream(Files.newInputStream(file))) {
            Customer copy = (Customer) in.readObject();      // deserialization
            System.out.println("Original: " + original);
            System.out.println("Restored: " + copy);
            System.out.println("Same object? " + (copy == original));
        }
        Files.delete(file);
    }
}
Output
Saved to customer.ser
Original: Customer[Asha, points=1250, orders=[A-17, B-42], token=tok-9f2c]
Restored: Customer[Asha, points=1250, orders=[A-17, B-42], token=null]
Same object? false
  • The list of orders was serialized automatically because List.of(...) lists are serializable.
  • The transient token was skipped and came back as null.
  • The restored object is a new, equal-looking copy, not the same instance.

3. serialVersionUID

Every serializable class has a version number, serialVersionUID. During deserialization the stored value must match the class's current value, or an InvalidClassException is thrown.

  • If you do not declare it, the JVM computes one from the class structure, so harmless changes such as adding a method can break compatibility.
  • Declare it explicitly: private static final long serialVersionUID = 1L;
  • Change it only when you make an incompatible change to the serialized form.

4. Serialization and Inheritance

If a superclass is not serializable, its fields are not saved. During deserialization its no-argument constructor runs to initialise them, so it must exist and be accessible:

InheritanceSerialization.java
import java.io.*;

public class InheritanceSerialization {
    static class Person {                           // NOT Serializable
        String name = "default";
        Person() { System.out.println("  Person() constructor runs"); }
    }

    static class Employee extends Person implements Serializable {
        private static final long serialVersionUID = 1L;
        int empId;
        Employee(String name, int empId) { this.name = name; this.empId = empId; }
    }

    public static void main(String[] args) throws Exception {
        System.out.println("1. new Employee(...):");
        Employee e = new Employee("Ravi", 501);
        System.out.println("2. deserializing a copy:");
        Employee copy = roundTrip(e);
        System.out.println("empId (serializable class field): " + copy.empId);
        System.out.println("name  (non-serializable parent):  " + copy.name);
    }

    @SuppressWarnings("unchecked")
    static <T> T roundTrip(T obj) throws Exception {
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        try (ObjectOutputStream out = new ObjectOutputStream(bytes)) { out.writeObject(obj); }
        try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
            return (T) in.readObject();
        }
    }
}
Output
1. new Employee(...):
  Person() constructor runs
2. deserializing a copy:
  Person() constructor runs
empId (serializable class field): 501
name  (non-serializable parent):  default

The name set by the Employee constructor was lost because Person is not serializable. If a superclass is serializable, all subclasses are automatically serializable too.

5. NotSerializableException

Every object in the graph must be serializable (or the field holding it must be transient):

NotSerializableDemo.java
import java.io.*;

public class NotSerializableDemo {
    static class Address { String city = "Pune"; }               // not Serializable
    static class User implements Serializable {
        private static final long serialVersionUID = 1L;
        String name = "Meera";
        Address address = new Address();                         // breaks serialization
    }

    public static void main(String[] args) {
        try {
            InheritanceSerialization.roundTrip(new User());
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}
Output
java.io.NotSerializableException: NotSerializableDemo$Address

6. Custom Serialization and Records

Private writeObject and readObject methods let a class control its serialized form, for example to encode sensitive fields or validate data on the way in. Records are deserialized through their canonical constructor, so validation there always applies:

CustomSerialization.java
import java.io.*;
import java.util.Base64;

public class CustomSerialization {
    static class Account implements Serializable {
        private static final long serialVersionUID = 1L;
        private final String user;
        private transient String pin;                       // handled manually below

        Account(String user, String pin) { this.user = user; this.pin = pin; }

        private void writeObject(ObjectOutputStream out) throws IOException {
            out.defaultWriteObject();                       // write normal fields
            out.writeUTF(Base64.getEncoder().encodeToString(pin.getBytes()));   // "encrypt" (demo only)
        }

        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            in.defaultReadObject();
            pin = new String(Base64.getDecoder().decode(in.readUTF()));
            if (user == null || user.isBlank()) throw new InvalidObjectException("user missing");   // validate
        }
        public String toString() { return user + " / pin length " + pin.length(); }
    }

    record Point(int x, int y) implements Serializable {
        Point {                                             // records deserialize via this constructor
            if (x < 0 || y < 0) throw new IllegalArgumentException("negative coordinate");
        }
    }

    public static void main(String[] args) throws Exception {
        System.out.println(InheritanceSerialization.roundTrip(new Account("asha", "4821")));
        System.out.println(InheritanceSerialization.roundTrip(new Point(3, 4)));
    }
}
Output
asha / pin length 4
Point[x=3, y=4]

Other hooks include writeReplace/readResolve (substitute another object, as singletons do) and serialPersistentFields.

7. Serializable vs Externalizable

AspectSerializableExternalizable
Methods to implementNone (marker)writeExternal and readExternal
What is writtenAll non-transient fields, automaticallyOnly what you write explicitly
Constructor on readNot called (except non-serializable parents)Public no-arg constructor required and called
transientRespectedIrrelevant

8. Security and Modern Alternatives

Deserializing untrusted data is dangerous: crafted byte streams can trigger "gadget chains" in classes on the classpath and lead to remote code execution. This has caused many serious vulnerabilities.

  • Never deserialize data from untrusted sources with ObjectInputStream.
  • If you must, use deserialization filters (ObjectInputFilter, Java 9+, and context-specific filters in Java 17) to allow only expected classes.
  • For data exchange and storage, prefer JSON (Jackson, Gson), Protocol Buffers, Avro or similar formats, which are language-neutral and safer.

9. Summary

  • Serialization turns an object graph into bytes; deserialization creates a new object from them.
  • Implement Serializable, declare a serialVersionUID, and mark fields that should not be saved as transient.
  • Non-serializable superclasses need a no-arg constructor; non-serializable fields cause NotSerializableException.
  • Customise with writeObject/readObject; records deserialize through their constructor.
  • Never deserialize untrusted input; prefer JSON or binary schema formats for data exchange.

Related Tutorials