Skip to content

fix(java): support copy of non-serializable JDK classes such as java.lang.Package#3797

Open
sjahedhussini wants to merge 7 commits into
apache:mainfrom
sjahedhussini:fix-issue-2941
Open

fix(java): support copy of non-serializable JDK classes such as java.lang.Package#3797
sjahedhussini wants to merge 7 commits into
apache:mainfrom
sjahedhussini:fix-issue-2941

Conversation

@sjahedhussini

@sjahedhussini sjahedhussini commented Jun 28, 2026

Copy link
Copy Markdown

Why?

Fory.copy(obj) throws UnsupportedOperationException: Class java.lang.Package doesn't support serialization (wrapped in CopyException) when the object graph
contains a non-Serializable JDK class, even though the same object round-trips
fine through serialize + deserialize. This makes deep-copying common
real-world object graphs (e.g. Hibernate objects) fail.

What does this PR do?

Root cause: ClassResolver.getSerializerClass rejects non-Serializable JDK
classes via the checkJdkClassSerializable guard. The check is correct for
serialization, but because copy and serialize share the same per-class serializer
created in createSerializer, the guard fired at serializer-creation time and
broke copy() as collateral damage.

Fix: route these classes to a new NonSerializableSerializer. Its write/read
still throw UnsupportedOperationException (binary serialization behavior is
unchanged — the failure is simply deferred to write time), but it inherits the
field-copy implementation from AbstractObjectSerializer, so Fory.copy() now
works. Transient fields are still copied (e.g. HashMap's size/table), since
the inherited copy path copies all non-static fields.

NonSerializableSerializer is kept deliberately distinct from
CopyOnlyObjectSerializer: the latter blocks serialization for
registration-security reasons (remediable by registering the class), whereas a
non-Serializable JDK class is intrinsically non-serializable and not remediable
by registration, so the failure semantics differ. The new serializer is also
registered in GraalvmSupport's default serializer set, consistent with every
other class returned by getSerializerClass.

Files changed:

  • resolver/ClassResolver.java — route non-Serializable JDK classes to the new serializer instead of throwing.
  • serializer/NonSerializableSerializer.java — new serializer (copy supported, serialize unsupported).
  • platform/GraalvmSupport.java — register the new serializer for native-image builds.
  • test/ForyCopyTest.java — tests.

Behavior note: serialization of these classes now surfaces as
SerializationException wrapping UnsupportedOperationException, rather than the
raw UnsupportedOperationException thrown eagerly at serializer creation.

Related issues

Fixes issue #2941.

AI Contribution Checklist

  • Substantial AI assistance was used in this PR: yes

  • If yes, I included a completed AI Contribution Checklist in this PR description and the required AI Usage Disclosure.

  • If yes, my PR description includes the ai_review summary and screenshot evidence/links from both fresh reviewers on the current diff.

  • Substantial AI assistance was used: yes

  • I can explain and defend all important changes without AI help.

  • I reviewed AI-assisted code changes line by line before submission.

  • I completed line-by-line self-review first and fixed issues before requesting AI review.

  • I ran two fresh AI review agents on the current diff: one using .claude/skills/fory-code-review/SKILL.md and one without.

  • I addressed all AI review comments and repeated the loop until both reviewers reported no further actionable comments.

  • I attached evidence of the final clean AI review from both reviewers below.

  • I ran human verification and recorded evidence below.

  • I added/updated tests where required.

  • N/A — no protocol/wire-format change; no separate performance evidence required (see below).

  • I verified licensing and provenance compliance.

```
AI Usage Disclosure

  • substantial_ai_assistance: yes
  • scope: design drafting, code drafting, tests, PR rationale
  • affected_files_or_subsystems: java/fory-core — resolver/ClassResolver.java, serializer/NonSerializableSerializer.java (new), platform/GraalvmSupport.java, test/ForyCopyTest.java
  • ai_review: Completed line-by-line self-review and fixed issues first. Ran two fresh reviewers on the final diff — one using .claude/skills/fory-code-review/SKILL.md, one without. Addressed all actionable comments (duplicate-serializer justification, removal of issue/history references in comments, GraalVM registration, test assertion strength, typos) and reran both until neither reported further actionable comments.
  • ai_review_artifacts: Attached are the result of AI Agents. The claude command for independent agent is:
    Do a thorough, independent code review of the changes on my current branch
    versus main: git diff main...fix-issue-2941. Review every changed line for
    correctness, edge cases, performance, maintainability, test quality, and style.
    Do NOT use any project-specific review skill — I want a fresh general review.
    List concrete actionable issues.
    The claude command for SKILL based agent is:
    Review the changes on my current branch versus main using the guidance in
    .claude/skills/fory-code-review/SKILL.md. Diff: git diff main...fix-issue-2941.
    Go line by line and list any actionable issues.
Screenshot from 2026-06-28 02-06-37 Screenshot from 2026-06-28 01-58-47
  • human_verification: cd java && mvn -pl fory-core test -Dtest=org.apache.fory.ForyCopyTest — pass. cd java && mvn -pl fory-core test — pass. Local spotless:check could not run due to a google-java-format/JDK incompatibility (JCAnyPattern NoClassDefFoundError) affecting files unrelated to this change; relying on project CI for the format gate. Reviewed all results.
  • performance_verification: N/A — change only redirects serializer resolution for non-Serializable JDK classes that previously threw; existing hot paths unchanged.
  • provenance_license_confirmation: Apache-2.0-compatible provenance confirmed; no incompatible third-party code introduced. New file carries the standard ASF license header.
    ```

Does this PR introduce any user-facing change?

  • No public API change. Fory.copy() now succeeds where it previously threw; no signature or contract change.
  • No binary protocol compatibility change. Serialization of these classes is still refused (now at write time).

Benchmark

N/A — see performance_verification above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant