Skip to content

Commit

Permalink
Added support of unmarshalling and marshalling of pointers (#32)
Browse files Browse the repository at this point in the history
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 <daniel@paypaplane.com>
  • Loading branch information
danielbairstow97 and danielbairstow97 authored Jan 31, 2024
1 parent c610362 commit 9905247
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 1 deletion.
10 changes: 9 additions & 1 deletion consume.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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)
}
Expand Down
3 changes: 3 additions & 0 deletions serialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
23 changes: 23 additions & 0 deletions serialize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import (
"testing"
)

var (
heyStr = "hey"
)

type struct1 struct {
Foo int
Bar Struct2
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 <nil>, BarPtr <nil>": {
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) {
Expand Down
68 changes: 68 additions & 0 deletions unserialize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down

0 comments on commit 9905247

Please sign in to comment.