44

Consider the following situation:

There is a serialization file, created by the older version of the application. Unfortunately, the package has changed for the class, that has been serialized. And now I need to load the information from this file into the same class, but located in different package. This class has serialVersionUID defined and has not changed (i.e. is compatible).

Question: Is it possible to load the new class instances from this file using any tricks (except trivial copying the class into old package and then using the deserialization wrapper logic)? It is possible to use readResolve() to recover from moving/renaming the class? If not, please, explain why.

Community
  • 1
  • 1
dma_k
  • 10,431
  • 16
  • 76
  • 128

7 Answers7

60

It is possible:

class HackedObjectInputStream extends ObjectInputStream {

    public HackedObjectInputStream(InputStream in) throws IOException {
        super(in);
    }

    @Override
    protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
        ObjectStreamClass resultClassDescriptor = super.readClassDescriptor();

        if (resultClassDescriptor.getName().equals("oldpackage.Clazz"))
            resultClassDescriptor = ObjectStreamClass.lookup(newpackage.Clazz.class);

        return resultClassDescriptor;
    }
}

This also allows one to ignore serialVersionUIDs mismatch or even deserialize a class if its field structure was changed.

Igor Nardin
  • 1,611
  • 14
  • 12
  • Thank you for your comments, +1. Indeed, this can be a solution, but I have no influence on the module, that does deserialization. So I can only trick `.ser` files, or the serialized class. – dma_k Oct 13 '10 at 11:17
  • 2
    This should be the accepted answer. It works, simply use this HackedObjectInputStream to read the object – digital illusion Aug 09 '12 at 19:16
  • 2
    I had some problems with flexibility trying to use it. So here my version if you need it http://stackoverflow.com/a/14608062/1085787 – gaponov Jan 30 '13 at 15:52
  • 1
    !Caution! Using this!. If the "oldPackage.clazz" in the serialized file has NOT the exact same construction or variables as the "newPackage.clazz" you will get a "StreamCorruptedException : wrong format X". So only use this if the serialized file represents exactly the newPackage.clazz! – X7S Dec 13 '16 at 09:07
  • what if I'm not using 'ObjectInputStream' but kryo.register(... new serilizer(..){..read..write..}) ? I can't see any readClassDescriptor I can override. – Achraf Amil Apr 07 '17 at 15:02
  • what all answers currently miss, are serialized arrays of objects with the old name. In that case the deserialization will fail for all given answers, as such class names are denoted with "[L;" instead. So comparing the class name with equal isn't enough any longer. Instead use a replace or create a regex to replace to classname. – Kristian Kraljic May 31 '17 at 00:04
  • Works perfectly! – Benjy Strauss Nov 20 '20 at 02:17
8

Question: Is it possible to load the new class instances from this file using any tricks (except trivial copying the class into old package and then using the deserialization wrapper logic)?

I don't think there are any other "tricks" you could use that don't involve at least a partial reimplementation of the serialization protocol.

Edit: there is in fact a hook that allows this if you control the deserialization process, see the other answer.

It is possible to use readResolve() to recover from moving/renaming the class? If not, please, explain why.

No, because the deserialization mechanism will fail much earlier, at the stage where it tries to locate the class that's being deserialized - it has no way of knowing that a class in a different package has a readResolve() method it's supposed to use.

Michael Borgwardt
  • 342,105
  • 78
  • 482
  • 720
  • I agree with this answer, Also I would like to add, that overriding `ObjectInputStream#resolveClass(ObjectStreamClass)` will not help, if returned class has different full name from one, that was originally requested (and that is the case). – dma_k Mar 01 '10 at 21:46
  • @dma_k: actually it looks to me like you could achieve what you want by overriding that method – Michael Borgwardt Mar 01 '10 at 22:10
  • 1
    [Comment from Igor](http://stackoverflow.com/questions/2358886/how-can-i-deserialize-the-object-if-it-was-moved-to-another-package-or-renamed/3916282#3916282) actually shows the solution but it turned out that I have no possibility to trick the de-/serialization process, but only the model. – dma_k Oct 31 '11 at 17:32
  • Couldn't he use reflection in some way? – dabicho Mar 09 '21 at 03:15
  • @dabicho: not in a way that makes sense if there's only one class involved. You cannot use reflection to temporarily chance the package of a class or anything like that. But if you want a reusable generic solution, you could use reflection and a custom annotation to have classes say "I'm the deserialization target for com.oldpackage.OldClass" and have the deserialization process pick that up automatically. But I doubt the problem occurs frequently enough in any project to justify that effort. – Michael Borgwardt Mar 09 '21 at 12:42
8

If you use Cygnus Hex Editor you can manually change the name of the package/class.

If the new name (always including the package) has the same size you can just replace the old name by the new name, but if the size has changed you need to update the first 2 chars before the name with new new length.

Right click the Standard Data Types and change to Big Endian.

The length is a Signed Word.

For example:

00 0E 70 61 63 6B 61 67 65 2E 53 61 6D 70 6C 65
.  .  p   a  c  k  a  g  e  .  S  a  m  p  l  e

is how package.Sample is writen. 00 0E means 14, the number of chars "package.Sample" has.

If we want to change to newpackage.Sample we replace that string to:

00 12 6E 65 77 70 61 63 6B 61 67 65 2E 53 61 6D 70 6C 65
.  .  n  e  w  p   a  c  k  a  g  e  .  S  a  m  p  l  e

00 12 means 18, the number of chars "newpackage.Sample" has.

And of course you can make a patcher to update this automatically.

Polyana Fontes
  • 3,156
  • 1
  • 27
  • 41
  • Does Cygnus Hex Editor allow to replace a string with another string (with different lengths)? – dma_k Oct 31 '11 at 17:33
6

Use this class instead of ObjectInputStream if your classes moved to another namespace.

class SafeObjectInputStream extends ObjectInputStream {
    private final String oldNameSpace;
    private final String newNameSpace;

    public SafeObjectInputStream(InputStream in, String oldNameSpace, String newNameSpace) throws IOException {
        super(in);
        this.oldNameSpace = oldNameSpace;
        this.newNameSpace = newNameSpace;
    }

    @Override
    protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
        ObjectStreamClass result = super.readClassDescriptor();
        try {
            if (result.getName().contains(oldNameSpace)) {
                String newClassName = result.getName().replace(oldNameSpace, newNameSpace);
                // Test the class exists
                Class localClass = Class.forName(newClassName);

                Field nameField = ObjectStreamClass.class.getDeclaredField("name");
                nameField.setAccessible(true);
                nameField.set(result, newClassName);

                ObjectStreamClass localClassDescriptor = ObjectStreamClass.lookup(localClass)
                Field suidField = ObjectStreamClass.class.getDeclaredField("suid");
                suidField.setAccessible(true);
                suidField.set(result, localClassDescriptor.getSerialVersionUID());
        }
        } catch(Exception e) {
            throw new IOException("Exception when trying to replace namespace", e);
        }
        return result;
    }

    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        if (desc.getName().contains(oldNameSpace)) {
            String newClassName = desc.getName().replace(oldNameSpace, newNameSpace);
            return Class.forName(newClassName);
        }
        return super.resolveClass(desc);
    }
}

You may use it as follows:

ObjectInputStream objectStream = new SafeObjectInputStream(inputStream, "org.oldnamespace", "org.newnamespace");
objectStream.readObject();

It won't fail with StreamCorruptedException if some of your classes change. Instead, it will try to load as many fields as possible. You may perform data validation/upgrade by implementing readObject method in your classes.

private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    // Validate read data here
}
Ivan Nikitin
  • 3,578
  • 27
  • 39
5

Probably your best bet is to recreate the old class (name, package and serial ID), read in the serialized form, then copy the data to an instance of the new object and reserialize that.

If you have a lot of these serialized objects, perhaps you could write a small script to do this so the "schema change" gets done in one go.

Another option is to resurrect the old class and implement its readResolve method to return an instance of the new class (perhaps by declaring a copy constructor). Personally I think I'd go for the schema change script and then delete the old class for good.

oxbow_lakes
  • 133,303
  • 56
  • 317
  • 449
  • +1 for the hint for injecting `readResolve()` into "old" class. But I assume with my question, that recovering of a class in old package is already considered and I ask for alternatives. – dma_k Mar 01 '10 at 21:50
2

I don't think it's possible to do what you want.

Format of serialization file keeps class names. In detail it has next structure:

AC ED

protocol version number

object data

object's class description

Class description has next format:

full class name

serial version unique ID (SHA1 from fields and methods signatures)

serialization options

field descriptors

When you try to deserialize object serialization mechanism compares class names first (and you don't pass this step), then it compares serialVersionUID's and only after passing these 2 steps deserializes object.

Roman
  • 64,384
  • 92
  • 238
  • 332
  • I suppose, you mean that `object data` does after `object's class description` in a stream. Thanks for the answer! – dma_k Mar 01 '10 at 21:54
  • No, as I read in Horstmann's "Core Java Volume I" object data goes first. – Roman Mar 02 '10 at 13:03
0

Addition to the hex editing way.

It worked for me and it was easier to replace old package name with the new ones instead of implementing class replacements overriding ObjectInputStream. Especially because there were anonymous classes as well.

Here is a script which replaces old class path with the new class path in a binary format.

Here is a content o my hexreplace.sh script:

#!/bin/bash
set -xue

OLD_STR=$(echo -n $1 | hexdump -ve '1/1 "%.2X"')
NEW_STR=$(echo -n $2 | hexdump -ve '1/1 "%.2X"')
SRC_FILE=$3
DST_FILE=$4

TMP_FILE=$(mktemp /tmp/bin.patched.XXXXXXXXXX)

[ -f $SRC_FILE ]

hexdump -ve '1/1 "%.2X"' "$SRC_FILE" | sed "s/$OLD_STR/$NEW_STR/g" | xxd -r -p > "$TMP_FILE"

mv "$TMP_FILE" "$DST_FILE"

Run

hexreplace.sh old.class.path new.class.path source_file destination_file

Script works correctly when source and destination files are the same.

Ivan Kovtun
  • 645
  • 1
  • 11
  • 18