From 3c63a378d4ce7b283cee9c8bf4a4a225a4e65cb5 Mon Sep 17 00:00:00 2001 From: EduardMikhrin Date: Fri, 3 Apr 2026 17:24:35 +0300 Subject: [PATCH 1/3] glightning: added support for omitempty tag for fields in json --- jrpc2/jsonrpc2.go | 12 +++++++++++- jrpc2/jsonrpc2_test.go | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/jrpc2/jsonrpc2.go b/jrpc2/jsonrpc2.go index 5637089..0d62ee1 100644 --- a/jrpc2/jsonrpc2.go +++ b/jrpc2/jsonrpc2.go @@ -375,7 +375,8 @@ func innerParseNamed(targetValue reflect.Value, params map[string]interface{}) e // check for the json tag match, as well a simple // lower case name match tag, _ := fT.Tag.Lookup("json") - if tag == key || key == strings.ToLower(fT.Name) { + name, _ := parseTag(tag) + if name == key || key == strings.ToLower(fT.Name) { found = true err := innerParse(targetValue, fVal, value) if err != nil { @@ -520,6 +521,15 @@ func innerParse(targetValue reflect.Value, fVal reflect.Value, value interface{} // i'm afraid that's a nil, my dear return nil } + if fVal.Type().Elem().Kind() != reflect.Struct { + n := reflect.New(fVal.Type().Elem()) + err := innerParse(targetValue, n.Elem(), value) + if err != nil { + return err + } + fVal.Set(n) + return nil + } if v.Kind() != reflect.Map { return NewError(nil, InvalidParams, fmt.Sprintf("Types don't match. Expected a map[string]interface{} from the JSON, instead got %s", v.Kind().String())) } diff --git a/jrpc2/jsonrpc2_test.go b/jrpc2/jsonrpc2_test.go index e63610e..c47f98e 100644 --- a/jrpc2/jsonrpc2_test.go +++ b/jrpc2/jsonrpc2_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" ) -//// This section (below) is for method json marshalling, +// // This section (below) is for method json marshalling, // with special emphasis on how the parameters get marshalled // and unmarshalled to/from 'Method' objects type HelloMethod struct { @@ -653,3 +653,42 @@ func TestServerRegistry(t *testing.T) { err_ = server.Unregister(method) assert.Equal(t, "Method not registered", err_.Error()) } + +type OmitEmptyMethod struct { + Required string `json:"required"` + Optional *string `json:"optional,omitempty"` + Count *uint32 `json:"count,omitempty"` +} + +func (m OmitEmptyMethod) New() interface{} { + return &OmitEmptyMethod{} +} + +func (m OmitEmptyMethod) Call() (jrpc2.Result, error) { + return nil, nil +} + +func (m OmitEmptyMethod) Name() string { + return "omit_empty" +} + +func TestParsingOmitEmptyFields(t *testing.T) { + requestJson := `{"id":1,"method":"omit_empty","params":{"required":"value","optional":"hello","count":7},"jsonrpc":"2.0"}` + s := jrpc2.NewServer() + s.Register(&OmitEmptyMethod{}) + + var result jrpc2.Request + err := s.Unmarshal([]byte(requestJson), &result) + assert.Nil(t, err) + + method, ok := result.Method.(*OmitEmptyMethod) + assert.True(t, ok) + assert.Equal(t, "omit_empty", method.Name()) + assert.Equal(t, "value", method.Required) + + assert.NotNil(t, method.Optional) + assert.Equal(t, "hello", *method.Optional) + + assert.NotNil(t, method.Count) + assert.Equal(t, uint32(7), *method.Count) +} From 2a023bffb641f5965b1121355e56bf523d1d551c Mon Sep 17 00:00:00 2001 From: EduardMikhrin Date: Fri, 3 Apr 2026 17:28:14 +0300 Subject: [PATCH 2/3] glightning: fixed comment for test --- jrpc2/jsonrpc2_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jrpc2/jsonrpc2_test.go b/jrpc2/jsonrpc2_test.go index c47f98e..ce02c68 100644 --- a/jrpc2/jsonrpc2_test.go +++ b/jrpc2/jsonrpc2_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" ) -// // This section (below) is for method json marshalling, +// This section (below) is for method json marshalling, // with special emphasis on how the parameters get marshalled // and unmarshalled to/from 'Method' objects type HelloMethod struct { From 6d73d13032a5d535d978256501f9777c7869ca6b Mon Sep 17 00:00:00 2001 From: EduardMikhrin Date: Mon, 6 Apr 2026 15:47:46 +0300 Subject: [PATCH 3/3] jsonrpc: refactoring the parsing --- jrpc2/jsonrpc2.go | 66 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/jrpc2/jsonrpc2.go b/jrpc2/jsonrpc2.go index 0d62ee1..af4a5ab 100644 --- a/jrpc2/jsonrpc2.go +++ b/jrpc2/jsonrpc2.go @@ -1,6 +1,7 @@ package jrpc2 import ( + "encoding" "encoding/hex" "encoding/json" "errors" @@ -312,6 +313,10 @@ func GetNamedParams(target Method) map[string]interface{} { } func isZero(x interface{}) bool { + if x == nil { + return true + } + return reflect.DeepEqual(x, reflect.Zero(reflect.TypeOf(x)).Interface()) } @@ -354,10 +359,7 @@ func ParseNamedParams(target Method, params map[string]interface{}) error { targetValue := reflect.Indirect(reflect.ValueOf(target)) err := innerParseNamed(targetValue, params) if err != nil { - fmt.Println("ERR") - fmt.Println(err) - fmt.Println(targetValue) - fmt.Println(params) + return err } return nil } @@ -372,12 +374,16 @@ func innerParseNamed(targetValue reflect.Value, params map[string]interface{}) e continue } fT := tType.Field(i) - // check for the json tag match, as well a simple - // lower case name match tag, _ := fT.Tag.Lookup("json") - name, _ := parseTag(tag) + + name, omit := parseTag(tag) + if name == key || key == strings.ToLower(fT.Name) { found = true + if omit && isZero(value) { + break + } + err := innerParse(targetValue, fVal, value) if err != nil { return err @@ -412,14 +418,12 @@ func innerParse(targetValue reflect.Value, fVal reflect.Value, value interface{} } // json.RawMessage escape hatch - var eg json.RawMessage - if fVal.Type() == reflect.TypeOf(eg) { + if strings.Contains(fVal.Type().String(), "RawMessage") { out, err := json.Marshal(value) if err != nil { return err } - jm := json.RawMessage(out) - fVal.Set(reflect.ValueOf(jm)) + fVal.Set(reflect.ValueOf(out).Convert(fVal.Type())) return nil } @@ -474,8 +478,12 @@ func innerParse(targetValue reflect.Value, fVal reflect.Value, value interface{} return nil } - av := value.([]interface{}) + av, ok := value.([]interface{}) + if !ok { + return NewError(nil, InvalidParams, fmt.Sprintf("Expected JSON array for slice field %s, but got %T", fVal.Type().Name(), value)) + } fVal.Set(reflect.MakeSlice(fVal.Type(), len(av), len(av))) + for i := range av { err := innerParse(targetValue, fVal.Index(i), av[i]) if err != nil { @@ -518,9 +526,39 @@ func innerParse(targetValue reflect.Value, fVal reflect.Value, value interface{} } case reflect.Ptr: if v.Kind() == reflect.Invalid { - // i'm afraid that's a nil, my dear return nil } + + umType := reflect.TypeOf((*json.Unmarshaler)(nil)).Elem() + tmType := reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() + ptrType := fVal.Type() + + if ptrType.Implements(umType) || reflect.PointerTo(ptrType.Elem()).Implements(umType) { + n := reflect.New(ptrType.Elem()) + data, err := json.Marshal(value) + if err != nil { + return err + } + if err := json.Unmarshal(data, n.Interface()); err != nil { + return err + } + fVal.Set(n) + return nil + } + + if ptrType.Implements(tmType) || reflect.PointerTo(ptrType.Elem()).Implements(tmType) { + s, ok := value.(string) + if !ok { + return NewError(nil, InvalidParams, fmt.Sprintf("Expected string input for %s.%s, but got %T", targetValue.Type().Name(), fVal.Type().Name(), value)) + } + n := reflect.New(ptrType.Elem()) + if err := n.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(s)); err != nil { + return err + } + fVal.Set(n) + return nil + } + if fVal.Type().Elem().Kind() != reflect.Struct { n := reflect.New(fVal.Type().Elem()) err := innerParse(targetValue, n.Elem(), value) @@ -530,9 +568,11 @@ func innerParse(targetValue reflect.Value, fVal reflect.Value, value interface{} fVal.Set(n) return nil } + if v.Kind() != reflect.Map { return NewError(nil, InvalidParams, fmt.Sprintf("Types don't match. Expected a map[string]interface{} from the JSON, instead got %s", v.Kind().String())) } + if fVal.IsNil() { // You need a new pointer object thing here // so allocate one with this voodoo-magique