From 3b2dd24ba034e082d186c4f735a468f7374e636d Mon Sep 17 00:00:00 2001 From: Team Humaki Date: Tue, 15 Sep 2026 13:55:39 -0700 Subject: [PATCH] json2: map by-position params onto struct fields --- v2/json2/json_test.go | 73 +++++++++++++++++++++++++++++++++++++++++++ v2/json2/server.go | 38 ++++++++++++++++++---- 2 files changed, 105 insertions(+), 6 deletions(-) diff --git a/v2/json2/json_test.go b/v2/json2/json_test.go index 23a19a8..edd863c 100644 --- a/v2/json2/json_test.go +++ b/v2/json2/json_test.go @@ -289,3 +289,76 @@ func TestDecodeNullResult(t *testing.T) { t.Error("Expected result to be nil, but got:", result) } } + +type notificationArgs struct { + Message string + Priority uint8 + Critical bool +} + +func TestReadRequestPositionalStruct(t *testing.T) { + body := `{"jsonrpc":"2.0","method":"Service1.Multiply","params":[4,2],"id":1}` + r, err := http.NewRequest("POST", "/", strings.NewReader(body)) + if err != nil { + t.Fatal(err) + } + cr := NewCodec().NewRequest(r) + var args Service1Request + if err := cr.ReadRequest(&args); err != nil { + t.Fatal(err) + } + if args.A != 4 || args.B != 2 { + t.Fatalf("got %+v, want A=4 B=2", args) + } + + s := rpc.NewServer() + s.RegisterCodec(NewCodec(), "application/json") + if err := s.RegisterService(new(Service1), ""); err != nil { + t.Fatal(err) + } + var res Service1Response + raw := map[string]interface{}{ + "jsonrpc": "2.0", + "method": "Service1.Multiply", + "params": []int{4, 2}, + "id": 1, + } + if err := executeRaw(t, s, raw, &res); err != nil { + t.Fatal(err) + } + if res.Result != 8 { + t.Fatalf("Multiply [4,2] got %d, want 8", res.Result) + } +} + +func TestReadRequestPositionalNotification(t *testing.T) { + body := `{"jsonrpc":"2.0","method":"X.Y","params":["Hello world", 10, false],"id":1}` + r, err := http.NewRequest("POST", "/", strings.NewReader(body)) + if err != nil { + t.Fatal(err) + } + cr := NewCodec().NewRequest(r) + var args notificationArgs + if err := cr.ReadRequest(&args); err != nil { + t.Fatal(err) + } + if args.Message != "Hello world" || args.Priority != 10 || args.Critical { + t.Fatalf("got %+v", args) + } +} + +func TestReadRequestByNameStillWorks(t *testing.T) { + body := `{"jsonrpc":"2.0","method":"Service1.Multiply","params":{"A":4,"B":2},"id":1}` + r, err := http.NewRequest("POST", "/", strings.NewReader(body)) + if err != nil { + t.Fatal(err) + } + cr := NewCodec().NewRequest(r) + var args Service1Request + if err := cr.ReadRequest(&args); err != nil { + t.Fatal(err) + } + if args.A != 4 || args.B != 2 { + t.Fatalf("got %+v, want A=4 B=2", args) + } +} diff --git a/v2/json2/server.go b/v2/json2/server.go index 49acad2..a5dc24d 100644 --- a/v2/json2/server.go +++ b/v2/json2/server.go @@ -10,6 +10,7 @@ import ( "encoding/json" "io" "net/http" + "reflect" "github.com/gorilla/rpc/v2" ) @@ -175,12 +176,12 @@ func (c *CodecRequest) ReadRequest(args interface{}) error { // Note: if c.request.Params is nil it's not an error, it's an optional member. // JSON params structured object. Unmarshal to the args object. if err := json.Unmarshal(*c.request.Params, args); err != nil { - // Clearly JSON params is not a structured object, - // fallback and attempt an unmarshal with JSON params as - // array value and RPC params is struct. Unmarshal into - // array containing the request struct. - params := [1]interface{}{args} - if err = json.Unmarshal(*c.request.Params, ¶ms); err != nil { + // Clearly JSON params is not a structured object. + // JSON-RPC 2.0 by-position params is an array of values in + // the method's expected order. First try the historical + // one-element wrap (array of a single object). If that + // fails, map array elements onto exported struct fields. + if err = unmarshalArrayParams(*c.request.Params, args); err != nil { c.err = &Error{ Code: E_INVALID_REQ, Message: err.Error(), @@ -192,6 +193,31 @@ func (c *CodecRequest) ReadRequest(args interface{}) error { return c.err } +// unmarshalArrayParams decodes a JSON-RPC 2.0 by-position params array. +func unmarshalArrayParams(params json.RawMessage, args interface{}) error { + wrap := [1]interface{}{args} + if err := json.Unmarshal(params, &wrap); err == nil { + return nil + } + rv := reflect.ValueOf(args) + if rv.Kind() != reflect.Ptr || rv.IsNil() { + return json.Unmarshal(params, &wrap) + } + ev := rv.Elem() + if ev.Kind() != reflect.Struct { + return json.Unmarshal(params, &wrap) + } + fields := make([]interface{}, 0, ev.NumField()) + for i := 0; i < ev.NumField(); i++ { + f := ev.Field(i) + if !f.CanSet() { + continue + } + fields = append(fields, f.Addr().Interface()) + } + return json.Unmarshal(params, &fields) +} + // WriteResponse encodes the response and writes it to the ResponseWriter. func (c *CodecRequest) WriteResponse(w http.ResponseWriter, reply interface{}) { res := &serverResponse{