Skip to content

[Java] Binary compatibility regression in Fory 1.3.0 for nested map-like types #3843

Description

@mandrean

Search before asking

Version

  • Writer: Fory Java 1.2.0
  • Reader: Fory Java 1.3.0
  • Java: Temurin JDK 21.0.5
  • OS: macOS

The same sequence succeeds when both writer and reader use Fory 1.2.0.

Component(s)

Java

Minimal reproduce step

This repro uses one POJO with a field whose concrete type is a HashMap subclass. It depends only on fory-core.

Use this pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>repro</groupId>
  <artifactId>fory-binary-compatibility-repro</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <fory.version>1.2.0</fory.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.apache.fory</groupId>
      <artifactId>fory-core</artifactId>
      <version>${fory.version}</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>3.6.1</version>
        <configuration>
          <mainClass>org.apache.fory.serializer.CompatibleSerializerTest</mainClass>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

1. Define the model and runner

Save as src/main/java/org/apache/fory/serializer/CompatibleSerializerTest.java:

package org.apache.fory.serializer;

import java.util.HashMap;
import java.util.HexFormat;
import org.apache.fory.Fory;
import org.apache.fory.config.CompatibleMode;
import org.apache.fory.config.Language;

public final class CompatibleSerializerTest {
  public static final class StringMap extends HashMap<String, String> {}

  public static final class StringMapDocument {
    public StringMap values;
  }

  public static void main(String[] args) {
    Fory fory =
        Fory.builder()
            .requireClassRegistration(false)
            .withCompatibleMode(CompatibleMode.COMPATIBLE)
            .withLanguage(Language.JAVA)
            .build();

    if ("write".equals(args[0])) {
      System.out.println(HexFormat.of().formatHex(fory.serialize(sample())));
      return;
    }

    byte[] oldBytes = HexFormat.of().parseHex(args[1]);
    StringMapDocument oldDocument = fory.deserialize(oldBytes, StringMapDocument.class);
    System.out.println("read 1.2.0 payload: " + oldDocument.values.get("key"));

    byte[] currentBytes = fory.serialize(oldDocument);
    StringMapDocument currentDocument =
        fory.deserialize(currentBytes, StringMapDocument.class);
    System.out.println("next round trip: " + currentDocument.values.get("key"));
  }

  private static StringMapDocument sample() {
    StringMapDocument document = new StringMapDocument();
    document.values = new StringMap();
    document.values.put("key", "value");
    return document;
  }
}

The package and enclosing class deliberately match the self-contained regression-test fixture, so the frozen payload can be consumed without a second repro-only source file.

2. Serialize with Fory 1.2.0 and dump the hex

HEX=$(mvn -q \
  -Dfory.version=1.2.0 \
  -Dexec.args=write \
  compile exec:java | tail -n 1)

echo "$HEX"

The generated 200-byte payload is:

00ff1e003ec03a392b329f10300245ba26d01e011c9a2ba38d489140168c92207df44e63c1340564ec89140168c923d99253e76538a1a6eb00fe8dc2a308d98036540ba1240416151615ff20022e30f622b2e61109700045ba26d01e011c9a2ba38d489140168c922065744e63c1340564ec89140168c923d99253e76538a1a6eb00f00101042e30f622b2e61109700045ba26d01e011c9a2ba38d489140168c922065744e63c1340564ec89140168c923d99253e76538a1a6eb00f024010c6b65791476616c7565

3. Read the 1.2.0 payload with Fory 1.3.0

Use the same class and the hex from step 2:

mvn -q \
  -Dfory.version=1.3.0 \
  -Dexec.args="read $HEX" \
  compile exec:java

The old payload itself is read successfully. The immediately following round-trip on the same Fory instance then fails.

4. Observe the regression

Fory 1.3.0 prints the successful old-payload read and then throws:

read 1.2.0 payload: value
[ERROR] ... Failed to deserialize input:
class org.apache.fory.serializer.CompatibleSerializerTest_StringMapForyCodecCompatible1_0
cannot be cast to class org.apache.fory.serializer.collection.MapLikeSerializer

As a control, run the same read sequence with Fory 1.2.0:

mvn -q \
  -Dfory.version=1.2.0 \
  -Dexec.args="read $HEX" \
  compile exec:java

It succeeds:

read 1.2.0 payload: value
next round trip: value

What did you expect to see?

Fory 1.3.0 should continue to read compatible payloads written by Fory 1.2.0 without corrupting serializer metadata used by later operations on the same Fory instance.

Expected output:

read 1.2.0 payload: value
next round trip: value

What did you see instead?

After Fory 1.3.0 reads the Fory 1.2.0 payload, the next valid round-trip fails with a ClassCastException from a generated reader:

org.apache.fory.exception.DeserializationException: Failed to deserialize input
Caused by: java.lang.ClassCastException:
class org.apache.fory.serializer.CompatibleSerializerTest_StringMapForyCodecCompatible1_0
cannot be cast to class org.apache.fory.serializer.collection.MapLikeSerializer

This is a binary-compatibility regression: the sequence passes entirely on Fory 1.2.0 but fails when the reader is upgraded to Fory 1.3.0.

Anything Else?

The failure requires reusing the same Fory instance:

  1. Fory 1.3.0 reads the compatible payload produced by Fory 1.2.0.
  2. The same instance serializes another instance of the same model.
  3. Deserializing that new payload resolves StringMap with a bean-compatible serializer where the generated reader requires a MapLikeSerializer.

Are you willing to submit a PR?

  • I'm willing to submit a PR!

Independent verification

I reran this exact reproduction locally:

  • Temurin JDK 21.0.5: Fory 1.2.0 succeeds; Fory 1.3.0 reads the old payload and then fails with the documented generated-codec-to-MapLikeSerializer cast.
  • The Fory 1.2.0 writer produced the documented 200-byte payload exactly (SHA-256: d98499ddd80efe22ab0e2d488783cdd01b72fee676d20aad68dfdd89c2de11b6).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions