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

Alanxtl 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 b12066608 feat(generic): validate generic mode and clarify config API 
(#3500)
b12066608 is described below

commit b12066608dd6babb11484ab24896a6c0b8b7313a
Author: Yuxuan Lv <[email protected]>
AuthorDate: Wed Aug 5 07:41:49 2026 +0800

    feat(generic): validate generic mode and clarify config API (#3500)
    
    Fail fast on an unknown generic mode when the reference is created, instead
    of silently falling back to the Map generalizer at runtime, and keep the
    accepted generic-mode set consistent between reference-creation validation
    and Triple's invoker routing.
    
    - add internal.IsGenericMode as the single source of truth for the accepted
      generic modes (true/gson/protobuf-json/bean, plus legacy protobuf), and
      build internal.ValidateGenericType on top of it, rejecting unknown values
    - wire the validation into ReferenceOptions.init
    - route protocol/triple/triple.go:isGenericCall through 
internal.IsGenericMode
      so a reference created with WithGenericType("bean") is recognized as 
generic
      and dispatched to the $invoke-capable NewTripleInvoker instead of
      NewDubbo3Invoker
    - clarify that generic mode and transport serialization are independent, and
      document the scope of generic.include.class, in WithGenericType and
      NewGenericService docs
    - cover the validation and isGenericCall (including bean) with table-driven 
tests
    
    Part of #3472
    
    Signed-off-by: bang <[email protected]>
    Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
---
 client/client.go               | 13 +++++++++++++
 client/options.go              | 19 +++++++++++++++++--
 internal/config.go             | 31 +++++++++++++++++++++++++++++++
 internal/config_test.go        | 29 +++++++++++++++++++++++++++++
 protocol/triple/triple.go      | 16 ++++++----------
 protocol/triple/triple_test.go |  3 +++
 6 files changed, 99 insertions(+), 12 deletions(-)

diff --git a/client/client.go b/client/client.go
index 73e8cb37d..b7406409f 100644
--- a/client/client.go
+++ b/client/client.go
@@ -136,6 +136,19 @@ func (cli *Client) NewService(service any, opts 
...ReferenceOption) (*Connection
 // NewGenericService creates a GenericService for making generic calls without 
pre-generated stubs.
 // The referenceStr parameter specifies the service interface name (e.g., 
"org.apache.dubbo.samples.UserProvider").
 //
+// Defaults:
+//   - generic mode (generalization format): "true" (Map). Override with 
client.WithGenericType.
+//   - transport serialization: Hessian2. Override with 
client.WithSerialization.
+//
+// The generic mode and the transport serialization are two independent 
settings:
+// the former decides how objects are generalized, the latter how they are 
encoded on
+// the wire. Passing an unknown generic mode returns an error instead of 
silently
+// falling back to the Map generalizer.
+//
+// Whether the "class" field is kept in Map (generic=true) results is 
controlled by
+// the global "generic.include.class" configuration (default true). It only 
affects
+// the Map generalizer and currently has no per-reference override.
+//
 // Example usage:
 //
 //     genericService, err := 
cli.NewGenericService("org.apache.dubbo.samples.UserProvider",
diff --git a/client/options.go b/client/options.go
index 32526a74f..6baa9a96a 100644
--- a/client/options.go
+++ b/client/options.go
@@ -139,6 +139,12 @@ func (refOpts *ReferenceOptions) init(opts 
...ReferenceOption) error {
                refConf.Serialization = constant.ProtobufSerialization
        }
 
+       // validate generic type, fail fast on unknown value instead of
+       // silently falling back to the Map generalizer at runtime
+       if err := internal.ValidateGenericType(refConf.Generic); err != nil {
+               return err
+       }
+
        return commonCfg.Verify(refOpts)
 }
 
@@ -361,8 +367,17 @@ func WithGeneric() ReferenceOption {
        }
 }
 
-// WithGenericType sets the generic serialization type for generic call
-// Valid values: "true" (default), "gson", "protobuf", "protobuf-json"
+// WithGenericType sets the generic mode (generalization format), which 
decides how
+// business objects are generalized into a generic structure.
+//
+// Valid values: "true" (default, Map), "gson", "protobuf-json", "bean".
+// "protobuf" is kept as a legacy compatibility value and is not recommended.
+// An unknown value is rejected when the reference is created (see init()), 
rather
+// than silently falling back to the Map generalizer.
+//
+// Note: the generic mode is different from the transport serialization set via
+// WithSerialization; the latter controls the on-the-wire encoding (hessian2 /
+// protobuf / json / msgpack).
 func WithGenericType(genericType string) ReferenceOption {
        return func(opts *ReferenceOptions) {
                opts.Reference.Generic = genericType
diff --git a/internal/config.go b/internal/config.go
index 600db114b..24fccc101 100644
--- a/internal/config.go
+++ b/internal/config.go
@@ -213,3 +213,34 @@ func ValidateRegistryIDs(ids []string, regs 
map[string]*global.RegistryConfig) e
        }
        return nil
 }
+
+// IsGenericMode reports whether a generic value denotes a generic call. It is 
the
+// single source of truth for the accepted generic modes so that callers 
(config
+// validation, protocol-level routing) stay in sync instead of each 
maintaining its
+// own list. An empty value means the call is not generic.
+//
+// Recognized modes: "true" (Map, default), "gson", "protobuf-json", "bean", 
and the
+// legacy "protobuf" (kept for compatibility, not recommended).
+func IsGenericMode(generic string) bool {
+       if generic == "" {
+               return false
+       }
+       return strings.EqualFold(generic, constant.GenericSerializationDefault) 
||
+               strings.EqualFold(generic, constant.GenericSerializationGson) ||
+               strings.EqualFold(generic, 
constant.GenericSerializationProtobufJson) ||
+               strings.EqualFold(generic, constant.GenericSerializationBean) ||
+               strings.EqualFold(generic, 
constant.GenericSerializationProtobuf)
+}
+
+// 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.
+//
+// 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) {
+               return nil
+       }
+       return fmt.Errorf("invalid generic type %q, valid values: true, gson, 
protobuf-json, bean", generic)
+}
diff --git a/internal/config_test.go b/internal/config_test.go
index 8e631ed34..5753a761e 100644
--- a/internal/config_test.go
+++ b/internal/config_test.go
@@ -326,3 +326,32 @@ func TestValidateMethodConfig(t *testing.T) {
                assert.Contains(t, err.Error(), "tps.limit.interval")
        })
 }
+
+func TestValidateGenericType(t *testing.T) {
+       tests := []struct {
+               name    string
+               generic string
+               wantErr bool
+       }{
+               {"empty means non-generic", "", false},
+               {"map default", constant.GenericSerializationDefault, false},
+               {"gson", constant.GenericSerializationGson, false},
+               {"protobuf-json", constant.GenericSerializationProtobufJson, 
false},
+               {"bean", constant.GenericSerializationBean, false},
+               {"protobuf legacy compat", 
constant.GenericSerializationProtobuf, false},
+               {"case insensitive", "TRUE", false},
+               {"unknown value", "bad-type", true},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       err := ValidateGenericType(tt.generic)
+                       if tt.wantErr {
+                               require.Error(t, err)
+                               assert.Contains(t, err.Error(), tt.generic)
+                       } else {
+                               assert.NoError(t, err)
+                       }
+               })
+       }
+}
diff --git a/protocol/triple/triple.go b/protocol/triple/triple.go
index 14afb3712..943497a30 100644
--- a/protocol/triple/triple.go
+++ b/protocol/triple/triple.go
@@ -21,7 +21,6 @@ import (
        "context"
        "fmt"
        "net/http"
-       "strings"
        "sync"
 )
 
@@ -151,7 +150,7 @@ func (tp *TripleProtocol) Refer(url *common.URL) 
base.Invoker {
        // Use NewTripleInvoker for:
        // 1. New protoc-gen-go-triple stub code (has ClientInfoKey)
        // 2. Non-IDL mode (IDLMode == NONIDL)
-       // 3. Generic call (generic=true/gson/protobuf/protobuf-json)
+       // 3. Generic call (generic=true/gson/protobuf/protobuf-json/bean)
        if ok || IDLMode == constant.NONIDL || isGenericCall {
                // new triple invoker supporting $invoke for generic calls
                invoker, err = NewTripleInvoker(url)
@@ -231,15 +230,12 @@ func (tp *TripleProtocol) HostHTTPHandler(url 
*common.URL, handler http.Handler)
        return nil
 }
 
-// isGenericCall checks if the generic parameter indicates a generic call
+// isGenericCall checks if the generic parameter indicates a generic call.
+// It delegates to internal.IsGenericMode so the accepted mode set stays in 
sync
+// with reference-creation validation (internal.ValidateGenericType), which is 
why
+// "bean" is recognized here too and routes to the $invoke-capable 
NewTripleInvoker.
 func isGenericCall(generic string) bool {
-       if generic == "" {
-               return false
-       }
-       return strings.EqualFold(generic, constant.GenericSerializationDefault) 
||
-               strings.EqualFold(generic, constant.GenericSerializationGson) ||
-               strings.EqualFold(generic, 
constant.GenericSerializationProtobuf) ||
-               strings.EqualFold(generic, 
constant.GenericSerializationProtobufJson)
+       return internal.IsGenericMode(generic)
 }
 
 func NewTripleProtocol() *TripleProtocol {
diff --git a/protocol/triple/triple_test.go b/protocol/triple/triple_test.go
index 53d97f296..1e91e1089 100644
--- a/protocol/triple/triple_test.go
+++ b/protocol/triple/triple_test.go
@@ -309,6 +309,9 @@ func Test_isGenericCall(t *testing.T) {
                {"protobuf-json", "protobuf-json", true},
                {"PROTOBUF-JSON", "PROTOBUF-JSON", true},
                {"Protobuf-Json", "Protobuf-Json", true},
+               {"bean", "bean", true},
+               {"BEAN", "BEAN", true},
+               {"Bean", "Bean", true},
 
                // invalid generic serialization types
                {"false", "false", false},

Reply via email to