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},