This is an automated email from the ASF dual-hosted git repository.

AlexStocks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git


The following commit(s) were added to refs/heads/develop by this push:
     new 81abe9ddb Fix(generic): complete typed result and generic mode 
handling (#3618)
81abe9ddb is described below

commit 81abe9ddb6d0ffd4a8fd7e1b895894e444654949
Author: XiaoFei <[email protected]>
AuthorDate: Wed Aug 12 15:40:53 2026 +0800

    Fix(generic): complete typed result and generic mode handling (#3618)
    
    * fix(generic): complete typed result handling
---
 client/client.go                        |   5 +-
 client/client_test.go                   | 114 +++++++++++++++++++++++
 client/options_test.go                  |  17 ++++
 filter/generic/filter.go                |  28 ++++--
 filter/generic/filter_test.go           | 159 ++++++++++++++++++++++++++++++--
 filter/generic/generalizer/bean.go      |   3 +
 filter/generic/generalizer/gson.go      |   4 +
 filter/generic/generalizer/gson_test.go |   4 +-
 filter/generic/service.go               |  51 +++++++---
 filter/generic/service_test.go          | 121 ++++++++++++++++++++++++
 filter/generic/util.go                  |  78 ++++++++++++++++
 internal/config.go                      |   8 +-
 internal/config_test.go                 |   3 +
 13 files changed, 559 insertions(+), 36 deletions(-)

diff --git a/client/client.go b/client/client.go
index b7406409f..21c39beb9 100644
--- a/client/client.go
+++ b/client/client.go
@@ -167,10 +167,13 @@ func (cli *Client) NewGenericService(referenceStr string, 
opts ...ReferenceOptio
        finalOpts = append(finalOpts, opts...)
 
        genericService := generic.NewGenericService(referenceStr)
-       _, err := cli.DialWithService(referenceStr, genericService, 
finalOpts...)
+       connection, err := cli.DialWithService(referenceStr, genericService, 
finalOpts...)
        if err != nil {
                return nil, err
        }
+       if err := 
genericService.SetGenericType(connection.refOpts.Reference.Generic); err != nil 
{
+               return nil, err
+       }
 
        return genericService, nil
 }
diff --git a/client/client_test.go b/client/client_test.go
index 837925a67..028e00e06 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -30,11 +30,16 @@ import (
 )
 
 import (
+       _ "dubbo.apache.org/dubbo-go/v3/cluster/cluster/available"
+       _ "dubbo.apache.org/dubbo-go/v3/cluster/router/condition"
        "dubbo.apache.org/dubbo-go/v3/common"
        "dubbo.apache.org/dubbo-go/v3/common/constant"
+       "dubbo.apache.org/dubbo-go/v3/common/extension"
+       _ "dubbo.apache.org/dubbo-go/v3/filter/graceful_shutdown"
        "dubbo.apache.org/dubbo-go/v3/global"
        "dubbo.apache.org/dubbo-go/v3/protocol/base"
        "dubbo.apache.org/dubbo-go/v3/protocol/result"
+       _ "dubbo.apache.org/dubbo-go/v3/proxy/proxy_factory"
 )
 
 type fakeInvoker struct {
@@ -43,6 +48,55 @@ type fakeInvoker struct {
        res            result.Result
 }
 
+type genericClientTestUser struct {
+       Name string `json:"name"`
+       Age  int    `json:"age"`
+}
+
+func (genericClientTestUser) JavaClassName() string {
+       return "org.apache.dubbo.test.User"
+}
+
+type genericResultProtocol struct {
+       *base.BaseProtocol
+}
+
+func (p *genericResultProtocol) Refer(url *common.URL) base.Invoker {
+       return &genericResultInvoker{BaseInvoker: base.NewBaseInvoker(url)}
+}
+
+type genericResultInvoker struct {
+       *base.BaseInvoker
+}
+
+func (i *genericResultInvoker) Invoke(_ context.Context, inv base.Invocation) 
result.Result {
+       mode := inv.GetAttachmentWithDefaultValue(
+               constant.GenericKey,
+               i.GetURL().GetParam(constant.GenericKey, 
constant.GenericSerializationDefault),
+       )
+       var response any
+       switch mode {
+       case constant.GenericSerializationGson, 
constant.GenericSerializationProtobufJson:
+               response = `{"name":"gsonUser","age":42}`
+       default:
+               response = map[string]any{"name": "mapUser", "age": 41}
+       }
+       reply := inv.Reply().(*any)
+       *reply = response
+       return &result.RPCResult{Rest: inv.Reply()}
+}
+
+func registerGenericResultProtocol(t *testing.T, protocolName string) {
+       t.Helper()
+       extension.SetProtocol(protocolName, func() base.Protocol {
+               proto := base.NewBaseProtocol()
+               return &genericResultProtocol{BaseProtocol: &proto}
+       })
+       t.Cleanup(func() {
+               extension.UnregisterProtocol(protocolName)
+       })
+}
+
 func (f *fakeInvoker) GetURL() *common.URL {
        return nil
 }
@@ -181,6 +235,66 @@ func TestConnectionCallPassesOptions(t *testing.T) {
        require.Same(t, &responseTrailer, trailerTarget)
 }
 
+func TestNewGenericServicePropagatesGenericTypeToTypedInvoke(t *testing.T) {
+       const protocolName = "generic-client-test"
+       registerGenericResultProtocol(t, protocolName)
+
+       cli, err := NewClient()
+       require.NoError(t, err)
+
+       service, err := cli.NewGenericService(
+               "org.apache.dubbo.test.UserProvider",
+               WithProtocol(protocolName),
+               WithURL(protocolName+"://127.0.0.1:1"),
+               WithClusterAvailable(),
+               WithGenericType(constant.GenericSerializationGson),
+       )
+       require.NoError(t, err)
+       if service.GenericType() != constant.GenericSerializationGson {
+               t.Errorf("generic service mode = %q, want %q", 
service.GenericType(), constant.GenericSerializationGson)
+       }
+
+       var user *genericClientTestUser
+       err = service.InvokeWithType(context.Background(), "getUser", nil, nil, 
&user)
+       require.NoError(t, err)
+       require.NotNil(t, user)
+       require.Equal(t, "gsonUser", user.Name)
+       require.Equal(t, 42, user.Age)
+}
+
+func TestNewGenericServiceInvokeKeepsRawResult(t *testing.T) {
+       tests := []struct {
+               name         string
+               protocolName string
+               generic      string
+       }{
+               {name: "gson", protocolName: "generic-client-raw-gson-test", 
generic: constant.GenericSerializationGson},
+               {name: "protobuf-json", protocolName: 
"generic-client-raw-protobuf-test", generic: 
constant.GenericSerializationProtobufJson},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       registerGenericResultProtocol(t, tt.protocolName)
+                       cli, err := NewClient()
+                       require.NoError(t, err)
+
+                       service, err := cli.NewGenericService(
+                               "org.apache.dubbo.test.UserProvider",
+                               WithProtocol(tt.protocolName),
+                               WithURL(tt.protocolName+"://127.0.0.1:1"),
+                               WithClusterAvailable(),
+                               WithGenericType(tt.generic),
+                       )
+                       require.NoError(t, err)
+
+                       res, err := service.Invoke(context.Background(), 
"getUser", nil, nil)
+
+                       require.NoError(t, err)
+                       require.JSONEq(t, `{"name":"gsonUser","age":42}`, 
res.(string))
+               })
+       }
+}
+
 func TestCallUnary(t *testing.T) {
        t.Run("success", func(t *testing.T) {
                invoker := &fakeInvoker{res: &result.RPCResult{}}
diff --git a/client/options_test.go b/client/options_test.go
index 9b8bf4505..ca2976366 100644
--- a/client/options_test.go
+++ b/client/options_test.go
@@ -726,6 +726,23 @@ func TestWithURL(t *testing.T) {
        processReferenceOptionsInitCases(t, cases)
 }
 
+func TestWithGenericType(t *testing.T) {
+       cases := []referenceOptionsInitCase{
+               {
+                       desc: "empty generic overrides default generic option",
+                       opts: []ReferenceOption{
+                               WithGeneric(),
+                               WithGenericType(""),
+                       },
+                       verify: func(t *testing.T, refOpts *ReferenceOptions, 
err error) {
+                               require.NoError(t, err)
+                               assert.Empty(t, refOpts.Reference.Generic)
+                       },
+               },
+       }
+       processReferenceOptionsInitCases(t, cases)
+}
+
 func TestWithFilter(t *testing.T) {
        cases := []referenceOptionsInitCase{
                {
diff --git a/filter/generic/filter.go b/filter/generic/filter.go
index bfada008b..b151bf7b7 100644
--- a/filter/generic/filter.go
+++ b/filter/generic/filter.go
@@ -172,7 +172,7 @@ func (f *genericFilter) OnResponse(_ context.Context, res 
result.Result, invoker
        if isGenericDisabled(configuredGeneric) {
                return res
        }
-       _, g, err := resolveGeneralizer(configuredGeneric, inv)
+       generic, g, err := resolveGeneralizer(configuredGeneric, inv)
        if err != nil {
                res.SetError(err)
                res.SetResult(nil)
@@ -196,27 +196,37 @@ func (f *genericFilter) OnResponse(_ context.Context, res 
result.Result, invoker
        if data == nil {
                return res
        }
-
-       // Check if data is a map type that needs to be deserialized
+       replyElem := replyValue.Elem()
        dataValue := reflect.ValueOf(data)
-       if dataValue.Kind() != reflect.Map && dataValue.Kind() != reflect.Slice 
{
-               // If data is not a map or slice, it's already a primitive 
type, no need to deserialize
+       if replyElem.Kind() == reflect.Interface && 
dataValue.Type().AssignableTo(replyElem.Type()) {
+               if dataValue.Kind() == reflect.Pointer && dataValue.Pointer() 
== replyValue.Pointer() {
+                       return res
+               }
+               replyElem.Set(dataValue)
+               return res
+       }
+
+       if !shouldRealizeTypedResult(data, generic) {
                return res
        }
 
        // Get the element type that the pointer points to
-       replyElemType := replyValue.Elem().Type()
+       replyElemType := replyElem.Type()
 
        // Realize the map/slice to the target struct using shared helper
        realized, err := realizeResult(data, replyElemType, g)
        if err != nil {
                logger.Warnf("[Filter][Generic] failed to deserialize generic 
result, err=%v", err)
+               res.SetError(err)
+               res.SetResult(nil)
                return res
        }
 
-       // Set the realized value to reply
-       if realized != nil {
-               replyValue.Elem().Set(reflect.ValueOf(realized))
+       if err := setRealizedReply(replyValue, realized); err != nil {
+               logger.Warnf("[Filter][Generic] failed to set generic result 
reply, err=%v", err)
+               res.SetError(err)
+               res.SetResult(nil)
+               return res
        }
 
        // Update the result with the deserialized reply
diff --git a/filter/generic/filter_test.go b/filter/generic/filter_test.go
index 5328b6ab5..081f15571 100644
--- a/filter/generic/filter_test.go
+++ b/filter/generic/filter_test.go
@@ -30,6 +30,8 @@ import (
 
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
+
+       "google.golang.org/protobuf/types/known/structpb"
 )
 
 import (
@@ -338,6 +340,10 @@ type mockUser struct {
        Address *mockAddress
 }
 
+func (mockUser) JavaClassName() string {
+       return "org.apache.dubbo.test.MockUser"
+}
+
 type mockAddress struct {
        City    string
        Country string
@@ -520,6 +526,148 @@ func TestFilter_OnResponse_WithSliceDeserialization(t 
*testing.T) {
        assert.Equal(t, 25, users[1].Age)
 }
 
+func TestFilter_OnResponse_WithTypedReplyByGenericMode(t *testing.T) {
+       filter := &genericFilter{}
+
+       ctrl := gomock.NewController(t)
+       defer ctrl.Finish()
+
+       mockInvoker := mock.NewMockInvoker(ctrl)
+       mockInvoker.EXPECT().GetURL().DoAndReturn(func() *common.URL {
+               return common.NewURLWithOptions(
+                       common.WithParams(url.Values{}),
+                       common.WithParamsValue(constant.GenericKey, 
constant.GenericSerializationDefault),
+               )
+       }).AnyTimes()
+
+       tests := []struct {
+               name   string
+               mode   string
+               result any
+       }{
+               {
+                       name: constant.GenericSerializationDefault,
+                       mode: constant.GenericSerializationDefault,
+                       result: map[string]any{
+                               "name": "mapUser",
+                               "age":  31,
+                       },
+               },
+               {
+                       name:   constant.GenericSerializationGson,
+                       mode:   constant.GenericSerializationGson,
+                       result: `{"name":"gsonUser","age":32}`,
+               },
+               {
+                       name: constant.GenericSerializationBean,
+                       mode: constant.GenericSerializationBean,
+                       result: mustGeneralize(t, 
generalizer.GetBeanGeneralizer(), mockUser{
+                               Name: "beanUser",
+                               Age:  33,
+                       }),
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       var user mockUser
+                       inv := invocation.NewRPCInvocationWithOptions(
+                               invocation.WithMethodName(constant.Generic),
+                               invocation.WithReply(&user),
+                               
invocation.WithAttachments(map[string]any{constant.GenericKey: tt.mode}),
+                       )
+                       res := &result.RPCResult{Rest: tt.result}
+
+                       newRes := filter.OnResponse(context.Background(), res, 
mockInvoker, inv)
+
+                       require.NoError(t, newRes.Error())
+                       assert.NotEmpty(t, user.Name)
+                       assert.NotZero(t, user.Age)
+                       assert.Same(t, &user, newRes.Result())
+               })
+       }
+}
+
+func TestFilter_OnResponse_WithProtobufJsonTypedReply(t *testing.T) {
+       filter := &genericFilter{}
+       invokeURL := common.NewURLWithOptions(
+               common.WithParams(url.Values{}),
+               common.WithParamsValue(constant.GenericKey, 
constant.GenericSerializationProtobufJson),
+       )
+
+       ctrl := gomock.NewController(t)
+       defer ctrl.Finish()
+
+       mockInvoker := mock.NewMockInvoker(ctrl)
+       mockInvoker.EXPECT().GetURL().Return(invokeURL).AnyTimes()
+
+       var reply structpb.Struct
+       inv := invocation.NewRPCInvocationWithOptions(
+               invocation.WithMethodName(constant.Generic),
+               invocation.WithReply(&reply),
+       )
+       res := &result.RPCResult{Rest: `{"name":"protoUser"}`}
+
+       newRes := filter.OnResponse(context.Background(), res, mockInvoker, inv)
+
+       require.NoError(t, newRes.Error())
+       assert.Equal(t, "protoUser", reply.Fields["name"].GetStringValue())
+       assert.Same(t, &reply, newRes.Result())
+}
+
+func TestFilter_OnResponse_KeepsRawResultForAnyReply(t *testing.T) {
+       filter := &genericFilter{}
+       invokeURL := common.NewURLWithOptions(
+               common.WithParams(url.Values{}),
+               common.WithParamsValue(constant.GenericKey, 
constant.GenericSerializationDefault),
+       )
+
+       ctrl := gomock.NewController(t)
+       defer ctrl.Finish()
+
+       mockInvoker := mock.NewMockInvoker(ctrl)
+       mockInvoker.EXPECT().GetURL().Return(invokeURL).AnyTimes()
+
+       for _, genericType := range []string{
+               constant.GenericSerializationGson,
+               constant.GenericSerializationProtobufJson,
+       } {
+               t.Run(genericType, func(t *testing.T) {
+                       const rawResult = `{"name":"rawUser"}`
+                       t.Run("direct result", func(t *testing.T) {
+                               var reply any
+                               inv := invocation.NewRPCInvocationWithOptions(
+                                       
invocation.WithMethodName(constant.Generic),
+                                       invocation.WithReply(&reply),
+                                       
invocation.WithAttachments(map[string]any{constant.GenericKey: genericType}),
+                               )
+
+                               newRes := 
filter.OnResponse(context.Background(), &result.RPCResult{Rest: rawResult}, 
mockInvoker, inv)
+
+                               require.NoError(t, newRes.Error())
+                               assert.JSONEq(t, rawResult, reply.(string))
+                               assert.JSONEq(t, rawResult, 
newRes.Result().(string))
+                       })
+
+                       t.Run("protocol reply pointer", func(t *testing.T) {
+                               reply := any(rawResult)
+                               inv := invocation.NewRPCInvocationWithOptions(
+                                       
invocation.WithMethodName(constant.Generic),
+                                       invocation.WithReply(&reply),
+                                       
invocation.WithAttachments(map[string]any{constant.GenericKey: genericType}),
+                               )
+                               res := &result.RPCResult{Rest: &reply}
+
+                               newRes := 
filter.OnResponse(context.Background(), res, mockInvoker, inv)
+
+                               require.NoError(t, newRes.Error())
+                               assert.JSONEq(t, rawResult, reply.(string))
+                               assert.Same(t, &reply, newRes.Result())
+                       })
+               })
+       }
+}
+
 func TestFilter_OnResponse_WithUnsupportedGenericMode(t *testing.T) {
        invokeURL := common.NewURLWithOptions(
                common.WithParams(url.Values{}),
@@ -538,8 +686,8 @@ func TestFilter_OnResponse_WithUnsupportedGenericMode(t 
*testing.T) {
        assert.Nil(t, newRes.Result())
 }
 
-// TestFilter_OnResponse_DeserializationError tests that OnResponse gracefully 
handles
-// deserialization failures by logging a warning and returning the original 
result.
+// TestFilter_OnResponse_DeserializationError tests that OnResponse returns an 
explicit error
+// when typed result deserialization fails.
 func TestFilter_OnResponse_DeserializationError(t *testing.T) {
        invokeUrl := common.NewURLWithOptions(
                common.WithParams(url.Values{}),
@@ -568,11 +716,10 @@ func TestFilter_OnResponse_DeserializationError(t 
*testing.T) {
 
                newRes := filter.OnResponse(context.Background(), res, 
mockInvoker, inv)
 
-               // OnResponse should return the original result when 
deserialization fails
-               // The user struct should remain unchanged (zero values)
+               // OnResponse should return an explicit error when typed 
deserialization fails.
                assert.Empty(t, user.Name)
                assert.Equal(t, 0, user.Age)
-               // The result should still be the original map
-               assert.Equal(t, mapResult, newRes.Result())
+               require.Error(t, newRes.Error())
+               assert.Nil(t, newRes.Result())
        })
 }
diff --git a/filter/generic/generalizer/bean.go 
b/filter/generic/generalizer/bean.go
index 8848f8966..a8f778d30 100644
--- a/filter/generic/generalizer/bean.go
+++ b/filter/generic/generalizer/bean.go
@@ -174,6 +174,9 @@ func (g *BeanGeneralizer) toDescriptor(obj any, visited 
map[uintptr]bool) *JavaB
 // fromDescriptor converts JavaBeanDescriptor to map for MapGeneralizer.Realize
 func (g *BeanGeneralizer) fromDescriptor(obj any) any {
        desc, ok := obj.(*JavaBeanDescriptor)
+       if ok && desc == nil {
+               return nil
+       }
        if !ok {
                if m, ok := obj.(map[any]any); ok {
                        desc = &JavaBeanDescriptor{Properties: 
make(map[any]any)}
diff --git a/filter/generic/generalizer/gson.go 
b/filter/generic/generalizer/gson.go
index 8be5f9da2..ae7135bf9 100644
--- a/filter/generic/generalizer/gson.go
+++ b/filter/generic/generalizer/gson.go
@@ -69,6 +69,10 @@ func (GsonGeneralizer) Realize(obj any, typ reflect.Type) 
(any, error) {
                return nil, perrors.Errorf("unexpected type of obj(=%T), wanted 
is string", obj)
        }
 
+       for typ.Kind() == reflect.Pointer {
+               typ = typ.Elem()
+       }
+
        // create the target object
        ret, ok := reflect.New(typ).Interface().(hessian.POJO)
        if !ok {
diff --git a/filter/generic/generalizer/gson_test.go 
b/filter/generic/generalizer/gson_test.go
index b1ec9a8fd..c25c88b79 100644
--- a/filter/generic/generalizer/gson_test.go
+++ b/filter/generic/generalizer/gson_test.go
@@ -87,9 +87,9 @@ func TestGsonPointer(t *testing.T) {
                Name:   "childName",
        }
 
-       m, err := mockMapGeneralizer.Generalize(c)
+       m, err := mockGsonGeneralizer.Generalize(c)
        require.NoError(t, err)
-       newC, err := mockMapGeneralizer.Realize(m, 
reflect.TypeFor[*mockGsonChild]())
+       newC, err := mockGsonGeneralizer.Realize(m, 
reflect.TypeFor[*mockGsonChild]())
        require.NoError(t, err)
        rMockChild, ok := newC.(*mockGsonChild)
        assert.True(t, ok)
diff --git a/filter/generic/service.go b/filter/generic/service.go
index ba68fa235..514a39dde 100644
--- a/filter/generic/service.go
+++ b/filter/generic/service.go
@@ -19,7 +19,6 @@ package generic
 
 import (
        "context"
-       "reflect"
 )
 
 import (
@@ -27,6 +26,7 @@ import (
 )
 
 import (
+       "dubbo.apache.org/dubbo-go/v3/common/constant"
        "dubbo.apache.org/dubbo-go/v3/filter/generic/generalizer"
 )
 
@@ -34,11 +34,12 @@ import (
 type GenericService struct {
        Invoke       func(ctx context.Context, methodName string, types 
[]string, args []hessian.Object) (any, error) `dubbo:"$invoke"`
        referenceStr string
+       generic      string
 }
 
 // NewGenericService returns a GenericService instance
 func NewGenericService(referenceStr string) *GenericService {
-       return &GenericService{referenceStr: referenceStr}
+       return &GenericService{referenceStr: referenceStr, generic: 
constant.GenericSerializationDefault}
 }
 
 // Reference gets referenceStr from GenericService
@@ -46,13 +47,30 @@ func (s *GenericService) Reference() string {
        return s.referenceStr
 }
 
+// SetGenericType sets the generic mode used by InvokeWithType to realize 
typed results.
+func (s *GenericService) SetGenericType(generic string) error {
+       if isGenericDisabled(generic) {
+               s.generic = generic
+               return nil
+       }
+       if _, err := getGeneralizer(generic); err != nil {
+               return err
+       }
+       s.generic = generic
+       return nil
+}
+
+// GenericType returns the generic mode used by InvokeWithType.
+func (s *GenericService) GenericType() string {
+       return s.generic
+}
+
 // InvokeWithType invokes the remote method and deserializes the result into 
the reply struct.
 // The reply parameter must be a non-nil pointer to the target type.
 //
-// Note: This method uses MapGeneralizer for deserialization, which means it 
only supports
-// the default map-based generic serialization (generic=true). If you are 
using other
-// serialization types like Gson or Protobuf-JSON, use the Invoke method 
directly and
-// handle deserialization manually.
+// InvokeWithType uses the service generic mode to realize the result. 
Supported modes are
+// true, gson, bean, protobuf-json, and the legacy protobuf mode (Map/Hessian 
semantics).
+// generic=false disables generic result realization and returns an explicit 
error.
 //
 // Example usage:
 //
@@ -69,6 +87,11 @@ func (s *GenericService) InvokeWithType(ctx context.Context, 
methodName string,
                return err
        }
 
+       g, err := s.getGeneralizer()
+       if err != nil {
+               return err
+       }
+
        // Call the underlying Invoke method
        result, err := s.Invoke(ctx, methodName, types, args)
        if err != nil {
@@ -79,19 +102,19 @@ func (s *GenericService) InvokeWithType(ctx 
context.Context, methodName string,
                return nil
        }
 
-       // Get the element type that the pointer points to
        replyType := replyValue.Elem().Type()
-
-       // Use MapGeneralizer to realize the map result to the target struct
-       g := generalizer.GetMapGeneralizer()
        realized, err := realizeResult(result, replyType, g)
        if err != nil {
                return err
        }
 
-       // Set the realized value to reply
-       if realized != nil {
-               replyValue.Elem().Set(reflect.ValueOf(realized))
+       return setRealizedReply(replyValue, realized)
+}
+
+func (s *GenericService) getGeneralizer() (generalizer.Generalizer, error) {
+       generic := s.GenericType()
+       if isGenericDisabled(generic) {
+               return nil, unsupportedTypedResultModeError(generic)
        }
-       return nil
+       return getGeneralizer(generic)
 }
diff --git a/filter/generic/service_test.go b/filter/generic/service_test.go
index 86ddbdc57..872a1ead3 100644
--- a/filter/generic/service_test.go
+++ b/filter/generic/service_test.go
@@ -27,6 +27,13 @@ import (
 
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
+
+       "google.golang.org/protobuf/types/known/structpb"
+)
+
+import (
+       "dubbo.apache.org/dubbo-go/v3/common/constant"
+       "dubbo.apache.org/dubbo-go/v3/filter/generic/generalizer"
 )
 
 type testUser struct {
@@ -36,6 +43,10 @@ type testUser struct {
        Address *testAddress
 }
 
+func (testUser) JavaClassName() string {
+       return "org.apache.dubbo.test.User"
+}
+
 type testAddress struct {
        City    string
        Country string
@@ -183,3 +194,113 @@ func TestGenericService_InvokeWithType(t *testing.T) {
                assert.Contains(t, err.Error(), "failed to deserialize result")
        })
 }
+
+func TestGenericService_InvokeWithTypeUsesGenericMode(t *testing.T) {
+       tests := []struct {
+               name   string
+               mode   string
+               result any
+       }{
+               {
+                       name: constant.GenericSerializationDefault,
+                       mode: constant.GenericSerializationDefault,
+                       result: map[string]any{
+                               "name": "mapUser",
+                               "age":  31,
+                       },
+               },
+               {
+                       name:   constant.GenericSerializationGson,
+                       mode:   constant.GenericSerializationGson,
+                       result: `{"name":"gsonUser","age":32}`,
+               },
+               {
+                       name: constant.GenericSerializationBean,
+                       mode: constant.GenericSerializationBean,
+                       result: mustGeneralize(t, 
generalizer.GetBeanGeneralizer(), testUser{
+                               Name: "beanUser",
+                               Age:  33,
+                       }),
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       service := NewGenericService("TestService")
+                       require.NoError(t, service.SetGenericType(tt.mode))
+                       service.Invoke = func(ctx context.Context, methodName 
string, types []string, args []hessian.Object) (any, error) {
+                               return tt.result, nil
+                       }
+
+                       var user testUser
+                       err := service.InvokeWithType(context.Background(), 
"getUser", nil, nil, &user)
+
+                       require.NoError(t, err)
+                       assert.NotEmpty(t, user.Name)
+                       assert.NotZero(t, user.Age)
+               })
+       }
+}
+
+func TestGenericService_InvokeWithTypeUsesProtobufJsonMode(t *testing.T) {
+       service := NewGenericService("TestService")
+       require.NoError(t, 
service.SetGenericType(constant.GenericSerializationProtobufJson))
+       service.Invoke = func(ctx context.Context, methodName string, types 
[]string, args []hessian.Object) (any, error) {
+               return `{"name":"protoUser"}`, nil
+       }
+
+       var reply structpb.Struct
+       err := service.InvokeWithType(context.Background(), "getUser", nil, 
nil, &reply)
+
+       require.NoError(t, err)
+       assert.Equal(t, "protoUser", reply.Fields["name"].GetStringValue())
+}
+
+func TestGenericService_InvokeWithTypeUsesGsonPointerReply(t *testing.T) {
+       service := NewGenericService("TestService")
+       require.NoError(t, 
service.SetGenericType(constant.GenericSerializationGson))
+       service.Invoke = func(ctx context.Context, methodName string, types 
[]string, args []hessian.Object) (any, error) {
+               return `{"name":"gsonUser","age":32}`, nil
+       }
+
+       var reply *testUser
+       err := service.InvokeWithType(context.Background(), "getUser", nil, 
nil, &reply)
+
+       require.NoError(t, err)
+       require.NotNil(t, reply)
+       assert.Equal(t, "gsonUser", reply.Name)
+       assert.Equal(t, 32, reply.Age)
+}
+
+func TestGenericService_InvokeWithTypeRejectsDisabledGenericMode(t *testing.T) 
{
+       tests := []struct {
+               name string
+               mode string
+       }{
+               {name: "empty", mode: ""},
+               {name: "false", mode: "false"},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       service := NewGenericService("TestService")
+                       require.NoError(t, service.SetGenericType(tt.mode))
+                       service.Invoke = func(ctx context.Context, methodName 
string, types []string, args []hessian.Object) (any, error) {
+                               t.Fatal("InvokeWithType should reject disabled 
generic mode before invoking")
+                               return nil, nil
+                       }
+
+                       var user testUser
+                       err := service.InvokeWithType(context.Background(), 
"getUser", nil, nil, &user)
+
+                       require.EqualError(t, err, `generic mode "`+tt.mode+`" 
does not support typed result`)
+               })
+       }
+}
+
+func mustGeneralize(t *testing.T, g generalizer.Generalizer, obj any) any {
+       t.Helper()
+       result, err := g.Generalize(obj)
+       require.NoError(t, err)
+       return result
+}
diff --git a/filter/generic/util.go b/filter/generic/util.go
index cab2c2ede..78f9f53cb 100644
--- a/filter/generic/util.go
+++ b/filter/generic/util.go
@@ -142,6 +142,84 @@ func realizeResult(data any, targetType reflect.Type, g 
generalizer.Generalizer)
        return realized, nil
 }
 
+func shouldRealizeTypedResult(data any, generic string) bool {
+       if data == nil {
+               return false
+       }
+       switch {
+       case strings.EqualFold(generic, constant.GenericSerializationGson),
+               strings.EqualFold(generic, 
constant.GenericSerializationProtobufJson),
+               strings.EqualFold(generic, constant.GenericSerializationBean):
+               return true
+       default:
+               kind := reflect.ValueOf(data).Kind()
+               return kind == reflect.Map || kind == reflect.Slice
+       }
+}
+
+func unsupportedTypedResultModeError(generic string) error {
+       return perrors.Errorf("generic mode %q does not support typed result", 
generic)
+}
+
+func setRealizedReply(replyValue reflect.Value, realized any) error {
+       if realized == nil {
+               return nil
+       }
+
+       target := replyValue.Elem()
+       value, ok := valueForAssignment(reflect.ValueOf(realized), 
target.Type())
+       if !ok {
+               return perrors.Errorf("failed to assign realized result of type 
%T to reply type %s", realized, target.Type())
+       }
+       target.Set(value)
+       return nil
+}
+
+func valueForAssignment(value reflect.Value, targetType reflect.Type) 
(reflect.Value, bool) {
+       if !value.IsValid() {
+               if canBeNil(targetType) {
+                       return reflect.Zero(targetType), true
+               }
+               return reflect.Value{}, false
+       }
+
+       if value.Type().AssignableTo(targetType) {
+               return value, true
+       }
+       if value.Type().ConvertibleTo(targetType) {
+               return value.Convert(targetType), true
+       }
+
+       if value.Kind() == reflect.Pointer {
+               if value.IsNil() {
+                       return reflect.Value{}, false
+               }
+               elem := value.Elem()
+               if elem.Type().AssignableTo(targetType) {
+                       return elem, true
+               }
+               if elem.Type().ConvertibleTo(targetType) {
+                       return elem.Convert(targetType), true
+               }
+       }
+
+       if targetType.Kind() == reflect.Pointer {
+               elemType := targetType.Elem()
+               if value.Type().AssignableTo(elemType) {
+                       ptr := reflect.New(elemType)
+                       ptr.Elem().Set(value)
+                       return ptr, true
+               }
+               if value.Type().ConvertibleTo(elemType) {
+                       ptr := reflect.New(elemType)
+                       ptr.Elem().Set(value.Convert(elemType))
+                       return ptr, true
+               }
+       }
+
+       return reflect.Value{}, false
+}
+
 // validateReplyPointer checks if the reply is a valid non-nil pointer.
 //
 // Parameters:
diff --git a/internal/config.go b/internal/config.go
index 24fccc101..a94e5e75f 100644
--- a/internal/config.go
+++ b/internal/config.go
@@ -233,14 +233,14 @@ func IsGenericMode(generic string) bool {
 }
 
 // ValidateGenericType validates the generic serialization type (generic mode).
-// An empty value means the call is not generic and is allowed. Unknown values 
fail
-// fast instead of silently falling back to the Map generalizer.
+// An empty value or "false" means the call is not generic and is allowed.
+// Unknown values fail fast instead of silently falling back to the Map 
generalizer.
 //
 // Valid values: "true" (Map, default), "gson", "protobuf-json", "bean".
 // "protobuf" is kept as a legacy compatibility value and is not recommended.
 func ValidateGenericType(generic string) error {
-       if generic == "" || IsGenericMode(generic) {
+       if generic == "" || strings.EqualFold(generic, "false") || 
IsGenericMode(generic) {
                return nil
        }
-       return fmt.Errorf("invalid generic type %q, valid values: true, gson, 
protobuf-json, bean", generic)
+       return fmt.Errorf("invalid generic type %q, valid values: true, false, 
gson, protobuf-json, bean, protobuf", generic)
 }
diff --git a/internal/config_test.go b/internal/config_test.go
index 5753a761e..a33b8a6c6 100644
--- a/internal/config_test.go
+++ b/internal/config_test.go
@@ -339,6 +339,7 @@ func TestValidateGenericType(t *testing.T) {
                {"protobuf-json", constant.GenericSerializationProtobufJson, 
false},
                {"bean", constant.GenericSerializationBean, false},
                {"protobuf legacy compat", 
constant.GenericSerializationProtobuf, false},
+               {"false disables generic", "false", false},
                {"case insensitive", "TRUE", false},
                {"unknown value", "bad-type", true},
        }
@@ -349,6 +350,8 @@ func TestValidateGenericType(t *testing.T) {
                        if tt.wantErr {
                                require.Error(t, err)
                                assert.Contains(t, err.Error(), tt.generic)
+                               assert.Contains(t, err.Error(), "false")
+                               assert.Regexp(t, `(?:^|,\s*)protobuf(?:,|$)`, 
err.Error())
                        } else {
                                assert.NoError(t, err)
                        }

Reply via email to