From 9905247093db4a4b3fc28a15b07e5c1797b03662 Mon Sep 17 00:00:00 2001 From: Daniel Bairstow Date: Thu, 1 Feb 2024 01:14:41 +1000 Subject: [PATCH] Added support of unmarshalling and marshalling of pointers (#32) During development of my own project I found some issues when trying to marshal php Nulls `N` into any Go struct fields. Was seeing uncaught panics such as `reflect: call of reflect.Value.Set on zero Value` if the value trying to be set on a struct field was Null. Additionally if a value was being set on a struct field of type reflect.Ptr that was nil a panic would occur when calling the .Set method on the struct field e.g. `reflect.Set: value of type string is not assignable to type *string` I've added testing to showcase the now supported functionality: * When serializing if a pointer value is Nil the MarshalNil function will be called * When unserializing if the value passed in is a nil interface the struct field will be left unset (set as default value) rather then panic * If unmarshalling to a pointer struct field, the field will first be instantiated before attempting to set the value of the pointer I used this Go playground to try base behaviour off of the `encoding/json` package: https://go.dev/play/p/5td0XqinlIP Co-authored-by: danielbairstow97 --- consume.go | 10 ++++++- serialize.go | 3 ++ serialize_test.go | 23 +++++++++++++++ unserialize_test.go | 68 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 1 deletion(-) diff --git a/consume.go b/consume.go index d941edf..fae6267 100644 --- a/consume.go +++ b/consume.go @@ -175,6 +175,11 @@ func setField(structFieldValue reflect.Value, value interface{}) error { } val := reflect.ValueOf(value) + if !val.IsValid() { + // structFieldValue will be set to default. + return nil + } + switch structFieldValue.Type().Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: structFieldValue.SetInt(val.Int()) @@ -211,7 +216,10 @@ func setField(structFieldValue reflect.Value, value interface{}) error { } structFieldValue.Set(arrayOfObjects) - + case reflect.Ptr: + // Instantiate structFieldValue. + structFieldValue.Set(reflect.New(structFieldValue.Type().Elem())) + return setField(structFieldValue.Elem(), value) default: structFieldValue.Set(val) } diff --git a/serialize.go b/serialize.go index 7e003ec..776f61a 100644 --- a/serialize.go +++ b/serialize.go @@ -247,6 +247,9 @@ func Marshal(input interface{}, options *MarshalOptions) ([]byte, error) { return MarshalStruct(input, options) case reflect.Ptr: + if value.IsNil() { + return MarshalNil(), nil + } return Marshal(value.Elem().Interface(), options) default: diff --git a/serialize_test.go b/serialize_test.go index 71c466b..24a03bc 100644 --- a/serialize_test.go +++ b/serialize_test.go @@ -6,6 +6,10 @@ import ( "testing" ) +var ( + heyStr = "hey" +) + type struct1 struct { Foo int Bar Struct2 @@ -33,6 +37,13 @@ type Struct3 struct { StringArray []string } +type Nillable struct { + Foo string + Bar Struct2 + FooPtr *string + BarPtr *Struct2 +} + type marshalTest struct { input interface{} output []byte @@ -167,6 +178,18 @@ var marshalTests = map[string]marshalTest{ []byte("O:8:\"stdClass\":3:{s:3:\"foo\";i:20;s:3:\"bar\";O:8:\"stdClass\":1:{s:3:\"qux\";d:7.89;}s:3:\"baz\";s:3:\"yay\";}"), getStdClassOnly(), }, + + // encode object with pointers + "Nillable{Foo string, Bar Struct2{Qux float64}, FooPtr *string, BarPtr *Struct2{Qux float64}": { + Nillable{"yay", Struct2{10}, &heyStr, &Struct2{}}, + []byte("O:8:\"Nillable\":4:{s:3:\"foo\";s:3:\"yay\";s:3:\"bar\";O:7:\"Struct2\":1:{s:3:\"qux\";d:10;}s:6:\"fooPtr\";s:3:\"hey\";s:6:\"barPtr\";O:7:\"Struct2\":1:{s:3:\"qux\";d:0;}}"), + nil, + }, + "Nillable{Foo string, Bar Struct2{Qux float64}, FooPtr , BarPtr ": { + Nillable{"", Struct2{}, nil, nil}, + []byte("O:8:\"Nillable\":4:{s:3:\"foo\";s:0:\"\";s:3:\"bar\";O:7:\"Struct2\":1:{s:3:\"qux\";d:0;}s:6:\"fooPtr\";N;s:6:\"barPtr\";N;}"), + nil, + }, } func TestMarshal(t *testing.T) { diff --git a/unserialize_test.go b/unserialize_test.go index ca783d9..9642e0e 100644 --- a/unserialize_test.go +++ b/unserialize_test.go @@ -537,6 +537,74 @@ func TestUnmarshalObjectWithTags(t *testing.T) { } } +func TestUnmarshalPointers(t *testing.T) { + data := "O:8:\"Nillable\":4:{s:3:\"foo\";s:3:\"yay\";s:3:\"bar\";O:7:\"Struct2\":1:{s:3:\"qux\";d:10;}s:6:\"fooPtr\";s:3:\"hey\";s:6:\"barPtr\";O:7:\"Struct2\":1:{s:3:\"qux\";d:0;}}" + target := &Nillable{ + Foo: "yay", + Bar: Struct2{ + Qux: 10, + }, + FooPtr: &heyStr, + BarPtr: &Struct2{ + Qux: 0, + }, + } + + var result Nillable + err := phpserialize.Unmarshal([]byte(data), &result) + expectErrorToNotHaveOccurred(t, err) + + if result.Foo != target.Foo { + t.Errorf("Expected %v, got %v for Foo", target.Foo, result.Foo) + } + + if result.Bar.Qux != target.Bar.Qux { + t.Errorf("Expected %v, got %v for Bar", target.Bar, result.Bar) + } + + if result.FooPtr == nil || *result.FooPtr != *target.FooPtr { + t.Errorf("Expected %v, got %v for FooPtr", *target.FooPtr, *result.FooPtr) + } + + if result.BarPtr == nil || result.BarPtr.Qux != result.BarPtr.Qux { + t.Errorf("Expected %v, got %v for BarPtr", target.BarPtr, result.BarPtr) + } + +} + +func TestUnmarshalPointersWithNull(t *testing.T) { + data := "O:8:\"Nillable\":4:{s:3:\"foo\";s:0:\"\";s:3:\"bar\";O:7:\"Struct2\":1:{s:3:\"qux\";d:0;}s:6:\"fooPtr\";N;s:6:\"barPtr\";N;}" + + target := &Nillable{ + Foo: "", + Bar: Struct2{ + Qux: 0, + }, + FooPtr: nil, + BarPtr: nil, + } + + var result Nillable + err := phpserialize.Unmarshal([]byte(data), &result) + expectErrorToNotHaveOccurred(t, err) + + if result.Foo != target.Foo { + t.Errorf("Expected %v, got %v for Foo", target.Foo, result.Foo) + } + + if result.Bar.Qux != target.Bar.Qux { + t.Errorf("Expected %v, got %v for Bar", target.Bar, result.Bar) + } + + if result.FooPtr != target.FooPtr { + t.Errorf("Expected %v, got %v for FooPtr", target.FooPtr, result.FooPtr) + } + + if result.BarPtr != target.BarPtr { + t.Errorf("Expected %v, got %v for BarPtr", target.BarPtr, result.BarPtr) + } +} + func TestUnmarshalObjectIntoMap(t *testing.T) { data := "O:7:\"struct1\":3:{s:3:\"foo\";i:10;s:3:\"bar\";O:7:\"Struct2\":1:{s:3:\"qux\";d:1.23;}s:3:\"baz\";s:3:\"yay\";}" var result map[interface{}]interface{}