Skip to Content
Course content

98: Custom Serialization

Click on the "Edit" button in the top corner of the screen to edit your slide content.

So, we've already covered the basics of the Serializable interface. On the surface, it seems like magic: you slap implements Serializable on a class, and Java handles the rest. But in a production environment, that "magic" is often too blunt an instrument. I've run into plenty of situations where the default mechanism either leaks sensitive data or crashes because it tries to serialize something that simply can't be serialized.

The Default Behavior Problem

Let's look at a scenario I dealt with recently. I had a UserSession class that held a user's ID and a session token. For this example, let's imagine it also holds a secretPin that the user entered to unlock their session. Here is how I started:

public class UserSession implements Serializable {
    private String username;
    private String secretPin;
    private long loginTime;

    public UserSession(String username, String secretPin) {
        this.username = username;
        this.secretPin = secretPin;
        this.loginTime = System.currentTimeMillis();
    }
    // Getters and setters omitted for brevity
}

I ran a quick test to save this object to a file and then read it back. It worked perfectly. But then I remembered that these files might be stored on a disk where other processes can see them. I opened the resulting .ser file in a hex editor and—surprise—the secretPin was sitting there in plain text. That's a massive security hole. Java's default serialization just dumps the field values as they are.

The Transient Trap

My first instinct was to use the transient keyword. I know you've seen this before: marking a field as transient tells Java, "Ignore this field during serialization."

private transient String secretPin;

I tried this, and the security hole was gone. The pin was no longer in the file. But I hit a new problem: when I deserialized the object, secretPin was null. In a real app, this would cause a NullPointerException the moment the user tried to perform an action requiring that PIN. We stopped the leak, but we broke the object's state. I needed a way to save the PIN, but not in plain text.

Writing Our Own Serialization Logic

This is where custom serialization comes in. Java provides two "magic" private methods that, if present in your class, the ObjectOutputStream and ObjectInputStream will call instead of (or in addition to) the default process. These are writeObject and readObject.

I decided to implement a simple encryption wrapper around the PIN during the save process. I'll use a fake encryption method here just to illustrate the flow, but the logic is what matters.

private void writeObject(ObjectOutputStream out) throws IOException {
    // 1. Perform the default serialization for non-transient fields
    out.defaultWriteObject();
    
    // 2. Manually handle the sensitive field
    String encryptedPin = encrypt(this.secretPin); 
    out.writeObject(encryptedPin);
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    // 1. Read the non-transient fields back in
    in.defaultReadObject();
    
    // 2. Read the encrypted string and decrypt it back into the field
    String encryptedPin = (String) in.readObject();
    this.secretPin = decrypt(encryptedPin);
}

private String encrypt(String data) { return "ENC-" + data; } // Simple mock
private String decrypt(String data) { return data.replace("ENC-", ""); } // Simple mock

Notice a few things here. First, I'm still calling out.defaultWriteObject() and in.defaultReadObject(). I do this because I don't want to manually write every single field (like username and loginTime) if I don't have to. I let Java handle the boring stuff and I only step in to handle the secretPin.

Second, these methods must be private. It feels counterintuitive, but the serialization API uses reflection to find these specific method signatures. If you make them public, the JVM won't treat them as the special hooks they are.

By doing this, I've achieved the best of both worlds: the sensitive data is protected on disk, but the object is fully restored to its functional state once it's loaded back into memory. It's a powerful pattern, especially when you're dealing with legacy systems or complex objects that contain non-serializable resources like database connections or network sockets that need to be re-initialized upon wake-up.




📋 Practical Task

Exercise: Implementing a Compressed GameSave System

You are building a game where the GameSave object contains a playerData string that can become massive (containing thousands of coordinates and item IDs). To save disk space, you need to compress this string during serialization and decompress it during deserialization.

Requirements:

  • Create a class GameSave that implements Serializable.
  • Include a String playerName (serialize normally) and a transient String playerData (serialize customly).
  • Implement writeObject to compress the playerData. For the purpose of this exercise, "compressing" can simply mean converting the string to a Base64 encoded string (use java.util.Base64).
  • Implement readObject to decode the Base64 string back into the playerData field.
  • Ensure you use defaultWriteObject() and defaultReadObject() to handle the playerName.

Verification: Write a main method that creates a GameSave object, serializes it to a file, reads it back, and prints the playerData to verify that the original content was recovered correctly.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.