Skip to content

fix: preserve generic parameter types during deserialization to fix List<Byte> → List<Integer> [3.3]#16198

Open
uuuyuqi wants to merge 5 commits intoapache:3.3from
uuuyuqi:fix/generic-param-type-3.3
Open

fix: preserve generic parameter types during deserialization to fix List<Byte> → List<Integer> [3.3]#16198
uuuyuqi wants to merge 5 commits intoapache:3.3from
uuuyuqi:fix/generic-param-type-3.3

Conversation

@uuuyuqi
Copy link
Copy Markdown

@uuuyuqi uuuyuqi commented Apr 9, 2026

Closes #16197

Summary

When RPC methods have parameters like List<Byte>, Map<String, Byte>, or List<Short>, the provider-side deserialization incorrectly produces List<Integer> / Map<String, Integer> because the generic type information is discarded during deserialization.

This PR fixes the issue across three layers:

  • Model layer: Add getGenericParameterTypes() to MethodDescriptor (default method for backward compat) and implement it in ReflectionMethodDescriptor to expose Type[] for method parameters.
  • Protocol layer: Update DecodeableRpcInvocation.drawArgs() (Dubbo protocol) and ReflectionPackableMethod.WrapRequestUnpack (Triple protocol) to pass generic Type to the serialization framework when available.
  • Serialization layer: Fix Hessian2ObjectInput.readObject(Class, Type) to post-convert collection/map elements to the correct narrow number type (Byte, Short, Float).

No wire protocol changes needed. The fix leverages existing local reflection information on the provider side.

Changes

Layer File Change
Model MethodDescriptor.java Add default getGenericParameterTypes()
Model ReflectionMethodDescriptor.java Store and return generic parameter types
Dubbo Protocol DecodeableRpcInvocation.java Pass generic Type in drawArgs()
Triple Protocol ReflectionPackableMethod.java Pass generic Type in WrapRequestUnpack
Serialization API MultipleSerialization.java Add deserialize(url, type, clz, genericType, is) overload
Serialization API DefaultMultipleSerialization.java Implement the new overload
Serialization Impl Hessian2ObjectInput.java Post-convert collection/map elements to narrow types

Test plan

  • Unit tests for ReflectionMethodDescriptor.getGenericParameterTypes()
  • Unit tests for Hessian2ObjectInput.readObject(Class, Type) with List<Byte>, List<Short>, List<Float>, Map<String, Byte>
  • E2E tested with Dubbo protocol + hessian2: all Byte elements correctly deserialized
  • E2E tested with Triple protocol + hessian2: all Byte elements correctly deserialized
  • Verified with Arthas that new code paths are executed at runtime
  • Reproduced the bug on official Dubbo 3.3.6, confirmed the fix resolves it

Made with Cursor

When RPC methods have parameters like List<Byte>, Map<String, Byte>,
or List<Short>, the provider-side deserialization incorrectly produces
List<Integer> / Map<String, Integer> because the generic type info
is discarded.

This fix addresses the issue across three layers:

1. Model layer: add getGenericParameterTypes() to MethodDescriptor
   (default method for backward compat) and implement it in
   ReflectionMethodDescriptor to expose Type[] for parameters.

2. Protocol layer: update DecodeableRpcInvocation.drawArgs() (Dubbo
   protocol) and ReflectionPackableMethod.WrapRequestUnpack (Triple
   protocol) to pass generic Type to the serialization framework.

3. Serialization layer: fix Hessian2ObjectInput.readObject(Class, Type)
   to post-convert collection/map elements to the correct narrow
   number type (Byte, Short, Float).

No wire protocol changes needed.

Change-Id: Ifefdd0cd9719de380ef5b3b6076e51123781cbb8
Co-developed-by: Cursor <noreply@cursor.com>
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 9, 2026

Codecov Report

❌ Patch coverage is 82.35294% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.81%. Comparing base (8814afa) to head (8ef70bb).
⚠️ Report is 1 commits behind head on 3.3.

Files with missing lines Patch % Lines
...common/serialize/hessian2/Hessian2ObjectInput.java 81.81% 0 Missing and 2 partials ⚠️
...a/org/apache/dubbo/rpc/model/MethodDescriptor.java 0.00% 1 Missing ⚠️
...bo/rpc/protocol/dubbo/DecodeableRpcInvocation.java 80.00% 0 Missing and 1 partial ⚠️
...bbo/rpc/protocol/tri/ReflectionPackableMethod.java 87.50% 0 Missing and 1 partial ⚠️
.../dubbo/common/serialize/MultipleSerialization.java 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##                3.3   #16198      +/-   ##
============================================
+ Coverage     60.79%   60.81%   +0.02%     
- Complexity    11751    11774      +23     
============================================
  Files          1953     1954       +1     
  Lines         89119    89148      +29     
  Branches      13444    13450       +6     
============================================
+ Hits          54177    54217      +40     
+ Misses        29368    29360       -8     
+ Partials       5574     5571       -3     
Flag Coverage Δ
integration-tests-java21 32.17% <55.88%> (+0.05%) ⬆️
integration-tests-java8 32.27% <55.88%> (+0.10%) ⬆️
samples-tests-java21 32.17% <41.17%> (-0.01%) ⬇️
samples-tests-java8 29.76% <41.17%> (-0.06%) ⬇️
unit-tests-java11 59.03% <82.35%> (+<0.01%) ⬆️
unit-tests-java17 58.51% <82.35%> (-0.03%) ⬇️
unit-tests-java21 58.51% <82.35%> (-0.02%) ⬇️
unit-tests-java25 58.51% <82.35%> (+0.04%) ⬆️
unit-tests-java8 59.03% <82.35%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

zrlw
zrlw previously approved these changes Apr 10, 2026
Copy link
Copy Markdown
Contributor

@zrlw zrlw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Copy Markdown
Contributor

@oxsean oxsean left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a problem. Could you provide a complete demo to reproduce it?

When making an RPC call, the type of each value should be included during serialization, rather than being resolved by the provider. For generic calls, the type is handle by PojoUtils.
https://github.com/apache/dubbo/blob/3.3/dubbo-rpc/dubbo-rpc-api/src/main/java/org/apache/dubbo/rpc/filter/GenericFilter.java

@uuuyuqi
Copy link
Copy Markdown
Author

uuuyuqi commented Apr 10, 2026

@oxsean Thanks for the review! Here is the complete reproduction demo:

Demo repo: https://github.com/uuuyuqi/dubbo3-list-byte-bug-demo

Clone, start Nacos, run mvn spring-boot:run for provider & consumer, then curl http://localhost:8082/testByte. With Dubbo 3.3.6, all Byte elements become Integer on the provider side.

Root cause explained

The issue is specific to hessian2 serialization with narrow number types (Byte, Short, Float) inside collections/maps. It is not about generic calls or PojoUtils — this happens with normal strongly-typed RPC calls.

Here is why:

  1. Hessian2 does not preserve the concrete type for small integers during serialization. When hessian2 serializes a Byte value like (byte) 1, it writes it as a compact integer (single-byte encoding 0x91). There is no type tag distinguishing Byte from Integer — it's just a number in the wire format. This is by design in the hessian2 protocol for compactness.

  2. During deserialization, hessian2 reads this compact integer and produces Integer by default, because it has no information about the target element type. The call chain is:

    DecodeableRpcInvocation.drawArgs()
      → in.readObject(List.class)          // only raw Class, no generic info
        → hessian2 deserializes the list
          → reads each element as Integer   // no way to know it should be Byte
    
  3. The generic type information IS available via Method.getGenericParameterTypes() on the provider side (local reflection, no wire transmission needed), but Dubbo never passes it to the serialization layer. The ObjectInput.readObject(Class, Type) API already exists in Dubbo's serialization interface but was never utilized in the deserialization path.

This fix simply connects the existing dots:

  • ReflectionMethodDescriptor already calls method.getParameterTypes() → now also calls method.getGenericParameterTypes()
  • DecodeableRpcInvocation.drawArgs() already calls in.readObject(Class) → now calls in.readObject(Class, Type) when generic info is available
  • Hessian2ObjectInput.readObject(Class, Type) already exists but ignored the Type param → now uses it to post-convert narrow number types

No changes to the wire protocol, no impact on non-generic parameters (the Type equals the raw Class for non-parameterized types, so the original code path is used).

@zrlw
Copy link
Copy Markdown
Contributor

zrlw commented Apr 10, 2026

@uuuyuqi you'd better add testcase that the interface has complex object which has narrow number bytes fields. e.g.,

public interface DemoService {
    void processPerson(Person person);
}

public static class Person {
    String name;
    byte age;
    short height;
    float salary;
}

Thread.currentThread().getContextClassLoader()));
}
return readObject(cls);
T result = readObject(cls);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using readObject(Class cl, Class<?>... expectedTypes) to avoid a second conversion.

…ype)

Add NarrowNumberPojo with byte/short/float fields and List<Byte>/
Map<String, Byte> fields to verify that POJO deserialization via
readObject(Class, Type) is not affected by the generic type fix.

Change-Id: I07751b608827b03ad4ac7b02704b522751ecbea8
Co-developed-by: Cursor <noreply@cursor.com>
@uuuyuqi
Copy link
Copy Markdown
Author

uuuyuqi commented Apr 10, 2026

@zrlw Thanks for the suggestion! I've added a test case with NarrowNumberPojo that has byte age, short height, float salary fields, plus List<Byte> scores and Map<String, Byte> attributes fields.

The test (testReadObjectWithGenericType_pojoWithNarrowNumberFields) verifies that:

  • Primitive narrow number fields (byte, short, float) are deserialized correctly
  • List<Byte> and Map<String, Byte> fields inside the POJO are also deserialized correctly
  • The fix does not affect POJO deserialization in any way

This is expected because when the method parameter is a POJO (e.g., processPerson(Person person)), genericParameterTypes[i] equals pts[i] (both are Person.class), so the code falls through to the original readObject(Class) path — POJO fields are handled by hessian2's JavaDeserializer which already reads field type info from the class definition.

All 16 tests pass locally.

…ype handling

Use hessian2's built-in expectedTypes support instead of post-processing
conversion, as suggested in review.

Change-Id: I1cbb65a684f2f03aa7219826f1fa866b6d7d79cc
Co-developed-by: Cursor <noreply@cursor.com>
oxsean
oxsean previously approved these changes Apr 11, 2026
@uuuyuqi
Copy link
Copy Markdown
Author

uuuyuqi commented Apr 11, 2026

Now it‘s using Hessian2Input.readObject(Class cl, Class<?>... expectedTypes), eliminating the need for a secondary conversion. Updated.

@zrlw
Copy link
Copy Markdown
Contributor

zrlw commented Apr 13, 2026

jobs/testjob_1-result-java21.txt:[11/35] [dubbo-samples-triple-rest-springmvc:1/1] TEST FAILURE: Run tests failed, version: -Ddubbo.version=3.3.7-SNAPSHOT -Dspring.version=6.1.5 -Djava.version=21, please check logs: /home/runner/work/dubbo/dubbo/2-advanced/dubbo-samples-triple-rest/dubbo-samples-triple-rest-springmvc/target/logs
jobs/testjob_2-result-java21.txt:[11/35] [dubbo-samples-triple-rest-jaxrs:1/1] TEST FAILURE: Run tests failed, version: -Ddubbo.version=3.3.7-SNAPSHOT -Dspring.version=6.1.5 -Djava.version=21, please check logs: /home/runner/work/dubbo/dubbo/2-advanced/dubbo-samples-triple-rest/dubbo-samples-triple-rest-jaxrs/target/logs

@zrlw
Copy link
Copy Markdown
Contributor

zrlw commented Apr 13, 2026

2026-04-11T13:28:00.3369064Z org.apache.dubbo.remoting.http12.exception.DecodeException: status=500, Internal Server Error
2026-04-11T13:28:00.3369388Z at org.apache.dubbo.rpc.protocol.tri.h12.grpc.GrpcCompositeCodec.decode(GrpcCompositeCodec.java:105)
2026-04-11T13:28:00.3369710Z at org.apache.dubbo.rpc.protocol.tri.h12.grpc.GrpcCompositeCodec.decode(GrpcCompositeCodec.java:111)
2026-04-11T13:28:00.3370019Z at org.apache.dubbo.remoting.http12.message.HttpMessageDecoder.decode(HttpMessageDecoder.java:56)
2026-04-11T13:28:00.3370387Z at org.apache.dubbo.remoting.http12.message.DefaultListeningDecoder.decode(DefaultListeningDecoder.java:41)
2026-04-11T13:28:00.3371064Z at org.apache.dubbo.rpc.protocol.tri.h12.http2.GenericHttp2ServerTransportListener$DefaultFragmentListener.onFragmentMessage(GenericHttp2ServerTransportListener.java:131)
2026-04-11T13:28:00.3371666Z at org.apache.dubbo.remoting.http12.message.LengthFieldStreamingDecoder.invokeListener(LengthFieldStreamingDecoder.java:257)
2026-04-11T13:28:00.3372305Z at org.apache.dubbo.rpc.protocol.tri.h12.grpc.GrpcHttp2ServerTransportListener$DetermineMethodDescriptorListener.onFragmentMessage(GrpcHttp2ServerTransportListener.java:212)
2026-04-11T13:28:00.3372740Z at org.apache.dubbo.remoting.http12.message.LengthFieldStreamingDecoder.invokeListener(LengthFieldStreamingDecoder.java:257)
2026-04-11T13:28:00.3373164Z at org.apache.dubbo.remoting.http12.message.LengthFieldStreamingDecoder.processBody(LengthFieldStreamingDecoder.java:249)
2026-04-11T13:28:00.3373574Z at org.apache.dubbo.remoting.http12.message.LengthFieldStreamingDecoder.deliver(LengthFieldStreamingDecoder.java:172)
2026-04-11T13:28:00.3373971Z at org.apache.dubbo.remoting.http12.message.LengthFieldStreamingDecoder.decode(LengthFieldStreamingDecoder.java:79)
2026-04-11T13:28:00.3374517Z at org.apache.dubbo.rpc.protocol.tri.h12.grpc.GrpcHttp2ServerTransportListener$LazyFindMethodListener.onMessage(GrpcHttp2ServerTransportListener.java:180)
2026-04-11T13:28:00.3374949Z at org.apache.dubbo.rpc.protocol.tri.h12.AbstractServerTransportListener.doOnData(AbstractServerTransportListener.java:183)
2026-04-11T13:28:00.3375394Z at org.apache.dubbo.rpc.protocol.tri.h12.AbstractServerTransportListener.lambda$onData$1(AbstractServerTransportListener.java:168)
2026-04-11T13:28:00.3375715Z at org.apache.dubbo.common.threadpool.serial.SerializingExecutor.run(SerializingExecutor.java:111)
2026-04-11T13:28:00.3375996Z at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
2026-04-11T13:28:00.3376269Z at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
2026-04-11T13:28:00.3376537Z at org.apache.dubbo.common.threadlocal.InternalRunnable.run(InternalRunnable.java:39)
2026-04-11T13:28:00.3376670Z at java.base/java.lang.Thread.run(Thread.java:1583)
2026-04-11T13:28:00.3377099Z Caused by: com.alibaba.com.caucho.hessian.io.HessianProtocolException: expected map/object at org.apache.dubbo.rest.demo.pojo.User
2026-04-11T13:28:00.3377392Z at com.alibaba.com.caucho.hessian.io.AbstractDeserializer.error(AbstractDeserializer.java:141)
2026-04-11T13:28:00.3377737Z at com.alibaba.com.caucho.hessian.io.AbstractMapDeserializer.readObject(AbstractMapDeserializer.java:68)
2026-04-11T13:28:00.3378091Z at com.alibaba.com.caucho.hessian.io.CollectionDeserializer.readLengthList(CollectionDeserializer.java:121)
2026-04-11T13:28:00.3378351Z at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2347)
2026-04-11T13:28:00.3378765Z at org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectInput.readObject(Hessian2ObjectInput.java:137)
2026-04-11T13:28:00.3379306Z at org.apache.dubbo.common.serialize.DefaultSerializationExceptionWrapper$ProxyObjectInput.readObject(DefaultSerializationExceptionWrapper.java:170)
2026-04-11T13:28:00.3379761Z at org.apache.dubbo.common.serialize.DefaultMultipleSerialization.deserialize(DefaultMultipleSerialization.java:58)
2026-04-11T13:28:00.3380236Z at org.apache.dubbo.rpc.protocol.tri.ReflectionPackableMethod$WrapRequestUnpack.deserializeFromWrapper(ReflectionPackableMethod.java:521)
2026-04-11T13:28:00.3380632Z at org.apache.dubbo.rpc.protocol.tri.ReflectionPackableMethod$WrapRequestUnpack.unpack(ReflectionPackableMethod.java:505)
2026-04-11T13:28:00.3380852Z at org.apache.dubbo.rpc.model.WrapperUnPack.unpack(WrapperUnPack.java:37)
2026-04-11T13:28:00.3381098Z at org.apache.dubbo.rpc.model.PackableMethod.parseRequest(PackableMethod.java:73)
2026-04-11T13:28:00.3381414Z at org.apache.dubbo.rpc.protocol.tri.h12.grpc.GrpcCompositeCodec.decode(GrpcCompositeCodec.java:101)

Hessian2Input.readObject(Class, Class...) expectedTypes mechanism
only works correctly for simple types. For complex POJO types like User,
passing the class as expectedType causes CollectionDeserializer to use
a wrong deserializer, leading to HessianProtocolException.

Change-Id: If909026ae4f9633155ffd5bd5c7b318aa77f492b
Co-developed-by: Cursor <noreply@cursor.com>
@RainYuY
Copy link
Copy Markdown
Member

RainYuY commented Apr 13, 2026

I'm sorry, but I don't think this problem needs to be fixed. Because the real reason why we cannot serialize it is that the Hessian types do not include byte. So the byte and Integer use the same tag value. If you fix this one, it will have the same problem like List<List<Integer>>, so I think the best way is to fix it in the Hessian protocol (hard), or change the serialization protocol.

…pectedTypes filtering

Align the type filtering logic with hessian-lite's FieldDeserializer2Factory,
using isPrimitive() to cover all primitive wrapper types instead of only
narrow number types (Byte/Short/Float).

Change-Id: If4dd8f961ec3b3a8e4f2881a866f527777e7a47d
Co-developed-by: Cursor <noreply@cursor.com>
@zrlw
Copy link
Copy Markdown
Contributor

zrlw commented Apr 13, 2026

I'm sorry, but I don't think this problem needs to be fixed. Because the real reason why we cannot serialize it is that the Hessian types do not include byte. So the byte and Integer use the same tag value. If you fix this one, it will have the same problem like List<List<Integer>>, so I think the best way is to fix it in the Hessian protocol (hard), or change the serialization protocol.

how about List<Map<String, Byte>> or Map<String, List<Byte>>?

@uuuyuqi
Copy link
Copy Markdown
Author

uuuyuqi commented Apr 13, 2026

@zrlw Fixed. The previous implementation passed all generic type arguments as expectedTypes to readObject(Class, Class<?>...), but the expectedTypes mechanism in hessian-lite only works for primitive types. For complex POJOs (like User), it bypasses tag-driven deserialization and causes HessianProtocolException.

Now only isPrimitive() types are passed as expectedTypes, consistent with hessian-lite's FieldDeserializer2Factory. POJO types fall back to default tag-driven deserialization and are not affected.

@uuuyuqi
Copy link
Copy Markdown
Author

uuuyuqi commented Apr 13, 2026

@RainYuY You're right that Hessian protocol doesn't have a native byte type — byte and int share the same tag. But hessian-lite already handles this through readObject(Class, Class<?>... expectedTypes): when CollectionDeserializer receives expectedTypes, it calls Number.byteValue() / Number.shortValue() etc. to narrow the type.

The issue was that Dubbo's invocation chain didn't pass generic type info down to hessian-lite, so this existing conversion capability was never triggered. This fix simply propagates the generic parameter types that are already available via reflection, letting hessian-lite's own type narrowing logic work as designed. No new conversion logic is introduced.

For nested generics like List<List<Byte>>, yes, that's a limitation of hessian-lite's expectedTypes which only supports one level of type arguments. But that's a separate issue and not in scope here — this fix addresses the common single-level cases (List<Byte>, Map<String, Byte>, etc.) which are the most frequently encountered.

@zrlw zrlw added the type/enhancement Everything related with code enhancement or performance label Apr 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status/wait for another approve type/enhancement Everything related with code enhancement or performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] List<Byte>, List<Short>, Map<String, Byte> deserialized as List<Integer>, Map<String, Integer> on provider side

5 participants