1

I am trying to load data from Java object streams from another application. Since I do not have access to the source code of that application, I used jdeserialize to extract the class definitions from the object streams. In general this works fine. Unfortunately, some (older) streams seem to have been serialized using classes with different serial version IDs. Otherwise the classes are identical to the ones I already have (as extracted by jdeserialize). This is the error I get when deserializing the older streams:

java.io.InvalidClassException: tls.other.app.package.SomeClass; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2

When I change the serial version IDs in the existing classes I can deserialize the older streams, but of course the new ones are not working anymore. I want to deserialize both versions of the Java object streams with the same application.

I tried different things - without success so far:

  • I created two packages with the same classes, just different serial version IDs. I created my own ObjectInputStream, overwriting readClassDescriptor(). The I try to read the stream using the first package. When that fails with the error message above I read the stream again changing the package / class name inside of readClassDescriptor():

    @Override
    protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
        ObjectStreamClass resultClassDescriptor = super.readClassDescriptor();
        String className = resultClassDescriptor.getName();
        if(_alternatePackage && className.startsWith("tls.other.app.package")) {
            Class<?> localClass;
            className = className.replace("tls.other.app.package", "tls.other.app.package2");
            try {
                localClass = Class.forName(className); 
            } catch (ClassNotFoundException e) {
                logger.error("No local class for " + resultClassDescriptor.getName(), e);
                return resultClassDescriptor;
            }
            resultClassDescriptor = ObjectStreamClass.lookup(localClass);
        }
        return resultClassDescriptor;
    }
    

    If I try to load the old streams I get the following error message:

    java.io.StreamCorruptedException: invalid type code: 00

  • I tried to make the serial version ID dynamic doing something like this:

    private static final long serialVersionUID = JavaObjectStreamReader.getSerialVersionUID(IDBSClasses.PropertyValueDTO);
    

    Of coursem the serialVersionUID is static and only initialized once. Therefore, I tried to use different ClassLoaders to reload the class after the first deserialization fails. That did not work either.

Is there any way to deserialize the old and the new streams in the same application?

4
  • 1
    Moral of the story: don't change the serialVersionUID. Despite its name, it is not a version number. It should be left constant forever, and other means used to version classes, many of which are already built-in to Java Object Serialization. Commented Sep 24 at 7:15
  • 1
    I can not reproduce the problem of getting a java.io.StreamCorruptedException: invalid type code: 00 with an approach as you’ve shown. But note that letting this problem aside, your approach is unnecessarily complicated. When you replace the stream’s descriptor returned by super.readClassDescriptor() with a local version acquired by ObjectStreamClass.lookup(…), you already bypassed the version check because the local descriptor will always have the right id (matching the local class it was generated from), regardless of the class. So you don’t need different versions of the classes. Commented Sep 24 at 7:50
  • 5
    @user207421 Not true. Official Oracle doc says: "A SerialVersionUID identifies the unique original class version for which this class is capable of writing streams and from which it can read." -> It is supposed to change if and only if the change you did to the class will prevent earlier versions to be deserialized correctly, or if newer versions can not be deserialized by the older class. It is literally the compatibility guarantee for (de)serialization. See docs.oracle.com/javase/8/docs/platform/serialization/spec/… Paragraph 4.1 second sentence Commented Sep 24 at 7:56
  • @Omega It doesn't say anywhere that it must be changed, and there is an entire chapter on Versioning listing all the ways that the class can be changed compatibly; further, the readResolve() and writeReplace() methods provide even more ways not to disturb the serialVersionUID., Commented Sep 25 at 0:50

2 Answers 2

3

First of all, the serialVersionUID acts as the guarantee that deserialization will work as expected.

The official Oracle Docs describe it as

A SerialVersionUID identifies the unique original class version for which this class is capable of writing streams and from which it can read.

(https://docs.oracle.com/javase/8/docs/platform/serialization/spec/class.html)

So your serialVersionUID should differ only, if you changed a class in a way, that it will either be unable to deserialize older classes, or older classes may be unable to deserialize this one.

Generally, I would recommend you to look into different methods of serialization like Jackson or Kryo.

Coming back to your problem:

You are working with old code, so this can't be helped. If you're absolutely certain, that the two versions are compatible, you can trick the mechanism with a modified ObjectInputStream like so:

ObjectInputStream in = new ObjectInputStream(new FileInputStream("theEntity2.bin")) {
        @Override
        protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
            var actual = super.readClassDescriptor();
            var local = ObjectStreamClass.lookup(TheEntity.class);
            if (actual.getName().equals(local.getName())) {
                return local;
            }
            return actual;
        }
    };

Basically, you intercept the type descriptor of the class read from the stream. If it is the class you want to load, in this case TheEntity, you simply return the deserialization descriptor of your local class instead.
Now the stream believes it just read exactly the class your have and deserialization should work as expected (if they are actually compatible).
If you have even more versions of which some are incompatible, check the return of getSerialVersionUID() from actual and determine, if you can handle it or not.

The code is a simplified rewrite of this answer:
Make Java runtime ignore serialVersionUIDs?

Sign up to request clarification or add additional context in comments.

Comments

2

There are at least a couple solutions.

1. Override ObjectInputStream::readClassDescriptor

You can "lie" by replacing the ObjectStreamClass with one for the "local class" instead of the one built from the serialized object. Note this approach does not require having your own version of the class. And it should work if the serialVersionUID is truly the only difference between the local class and the serialized object's class.

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;

public class CustomObjectInputStream extends ObjectInputStream {

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

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

    // replace 'Data.class' with your real class
    if (Data.class.getName().equals(descriptor.getName())) {
      // assumes class implements 'Serializable'
      descriptor = ObjectStreamClass.lookup(Data.class);
    }
    return descriptor;
  }
}

This will need to be compiled against the library containing Data.class (placeholder value). Otherwise, modify the code to use Class::forName.

2. Patch the Code

You mentioned that you managed to create the same class, just with different serialVersionUIDs and packages. Do the same thing but keep your version of the class in the same package as the real class.

For example, if you end up with the following:

<project-dir>
│   lib.jar
│   
└───patches
    └───com
        └───example
                Data.class

Where lib.jar contains the "real" Data class and patches/com/example/Data.class contains your modified version, then you can launch the application in such a way that the latter will be picked up over the former.

Non-modular Library

If the same class appears in more than one entry on the class-path, then earlier entries take precedence.

java -cp patches:lib.jar <main-class>

Replace : with ; on Windows.

Modular Library

Assuming the module name is lib:

java -p lib.jar --patch-module lib=patches ...

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.