From 0572e13c18568c38ca694bb5c5c3ae2b8b79158f Mon Sep 17 00:00:00 2001 From: Yehezkiel Syamsuhadi Date: Tue, 7 Apr 2026 10:41:51 +1000 Subject: [PATCH 1/3] Adding ability to marshal a struct as if it is a map --- serialize.go | 11 ++++++++++- serialize_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/serialize.go b/serialize.go index 776f61a..e6fbea0 100644 --- a/serialize.go +++ b/serialize.go @@ -15,6 +15,8 @@ type MarshalOptions struct { // If this is true, then all struct names will be stripped from objects // and "stdClass" will be used instead. The default value is false. OnlyStdClass bool + // If this is true, then a struct will be marshalled as if it is a map. + MarshalStructAsMap bool } // DefaultMarshalOptions will create a new instance of MarshalOptions with @@ -174,7 +176,10 @@ func MarshalStruct(input interface{}, options *MarshalOptions) ([]byte, error) { visibleFieldCount-- continue } else if fieldName == "" { - fieldName = lowerCaseFirstLetter(typeOfValue.Field(i).Name) + fieldName = typeOfValue.Field(i).Name + if !options.MarshalStructAsMap { + fieldName = lowerCaseFirstLetter(fieldName) + } } buffer.Write(MarshalString(fieldName)) @@ -186,6 +191,10 @@ func MarshalStruct(input interface{}, options *MarshalOptions) ([]byte, error) { buffer.Write(m) } + if options.MarshalStructAsMap { + return []byte(fmt.Sprintf("a:%d:{%s}", visibleFieldCount, buffer.String())), nil + } + className := reflect.ValueOf(input).Type().Name() if options.OnlyStdClass { className = "stdClass" diff --git a/serialize_test.go b/serialize_test.go index 24a03bc..05b3b05 100644 --- a/serialize_test.go +++ b/serialize_test.go @@ -57,6 +57,13 @@ func getStdClassOnly() *phpserialize.MarshalOptions { return stdClassOnly } +func getMarshalStructAsMap() *phpserialize.MarshalOptions { + options := phpserialize.DefaultMarshalOptions() + options.MarshalStructAsMap = true + + return options +} + // These tests have been adapted from the wonderful work at: // https://github.com/mitsuhiko/phpserialize/blob/master/tests.py var marshalTests = map[string]marshalTest{ @@ -147,11 +154,21 @@ var marshalTests = map[string]marshalTest{ []byte("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\";}"), nil, }, + "struct1{Foo int, Bar Struct2{Qux float64}, hidden bool, Bar string} as map": { + struct1{10, Struct2{1.23}, true, "yay"}, + []byte("a:3:{s:3:\"Foo\";i:10;s:3:\"Bar\";a:1:{s:3:\"Qux\";d:1.23;}s:3:\"Baz\";s:3:\"yay\";}"), + getMarshalStructAsMap(), + }, "&struct1{Foo int, Bar Struct2{Qux float64}, hidden bool}": { &struct1{20, Struct2{7.89}, false, "yay"}, []byte("O:7:\"struct1\":3:{s:3:\"foo\";i:20;s:3:\"bar\";O:7:\"Struct2\":1:{s:3:\"qux\";d:7.89;}s:3:\"baz\";s:3:\"yay\";}"), nil, }, + "&struct1{Foo int, Bar Struct2{Qux float64}, hidden bool} as map": { + &struct1{20, Struct2{7.89}, false, "yay"}, + []byte("a:3:{s:3:\"Foo\";i:20;s:3:\"Bar\";a:1:{s:3:\"Qux\";d:7.89;}s:3:\"Baz\";s:3:\"yay\";}"), + getMarshalStructAsMap(), + }, // encode object with array of objects "struct3{ObjectArray Struct2{Qux float64}, IntArray {1, 2}, FloatArray {1.0, 2.0}, StringArray {'a', 'b'}}": { @@ -159,6 +176,11 @@ var marshalTests = map[string]marshalTest{ []byte("O:7:\"Struct3\":4:{s:11:\"objectArray\";a:2:{i:0;O:7:\"Struct2\":1:{s:3:\"qux\";d:1.1;}i:1;O:7:\"Struct2\":1:{s:3:\"qux\";d:2.2;}}s:8:\"intArray\";a:2:{i:0;i:1;i:1;i:2;}s:10:\"floatArray\";a:2:{i:0;d:1;i:1;d:2;}s:11:\"stringArray\";a:2:{i:0;s:1:\"a\";i:1;s:1:\"b\";}}"), nil, }, + "struct3{ObjectArray Struct2{Qux float64}, IntArray {1, 2}, FloatArray {1.0, 2.0}, StringArray {'a', 'b'}} as map": { + Struct3{[]Struct2{{1.1}, {2.2}}, []int64{1, 2}, []float64{1.0, 2.0}, []string{"a", "b"}}, + []byte("a:4:{s:11:\"ObjectArray\";a:2:{i:0;a:1:{s:3:\"Qux\";d:1.1;}i:1;a:1:{s:3:\"Qux\";d:2.2;}}s:8:\"IntArray\";a:2:{i:0;i:1;i:1;i:2;}s:10:\"FloatArray\";a:2:{i:0;d:1;i:1;d:2;}s:11:\"StringArray\";a:2:{i:0;s:1:\"a\";i:1;s:1:\"b\";}}"), + getMarshalStructAsMap(), + }, // encode object (struct with tags) "structTag{Bar int, Foo Struct2{Qux float64}, hidden bool, Balu string, Nilptr }": { @@ -166,6 +188,11 @@ var marshalTests = map[string]marshalTest{ []byte("O:9:\"structTag\":3:{s:3:\"bar\";O:7:\"Struct2\":1:{s:3:\"qux\";d:1.23;}s:3:\"foo\";i:10;s:3:\"baz\";s:3:\"yay\";}"), nil, }, + "structTag{Bar int, Foo Struct2{Qux float64}, hidden bool, Balu string, Nilptr } as map": { + structTag{Struct2{1.23}, 10, true, "yay", "", nil}, + []byte("a:3:{s:3:\"bar\";a:1:{s:3:\"Qux\";d:1.23;}s:3:\"foo\";i:10;s:3:\"baz\";s:3:\"yay\";}"), + getMarshalStructAsMap(), + }, // stdClassOnly "struct1{Foo int, Bar Struct2{Qux float64}, hidden bool}: OnlyStdClass = true": { From 57fc4e8323bfa18c19c53b329e25ab5a5dc97bcb Mon Sep 17 00:00:00 2001 From: Yehezkiel Syamsuhadi Date: Wed, 8 Apr 2026 11:06:48 +1000 Subject: [PATCH 2/3] Adding ability to unmarshal an associative array into a struct --- consume.go | 206 ++++++++++++++++++++++++++++++++++++----- unserialize.go | 19 +++- unserialize_test.go | 217 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 420 insertions(+), 22 deletions(-) diff --git a/consume.go b/consume.go index fae6267..7f96f2a 100644 --- a/consume.go +++ b/consume.go @@ -2,7 +2,9 @@ package phpserialize import ( "errors" + "fmt" "reflect" + "sort" "strconv" ) @@ -182,44 +184,120 @@ func setField(structFieldValue reflect.Value, value interface{}) error { switch structFieldValue.Type().Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - structFieldValue.SetInt(val.Int()) + if val.CanInt() { + structFieldValue.SetInt(val.Int()) + } else { + intVal, err := strconv.ParseInt(fmt.Sprintf("%v", val.Interface()), 10, 64) + if err != nil { + return err + } + structFieldValue.SetInt(intVal) + } case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - structFieldValue.SetUint(val.Uint()) + if val.CanUint() { + structFieldValue.SetUint(val.Uint()) + } else { + uintVal, err := strconv.ParseUint(fmt.Sprintf("%v", val.Interface()), 10, 64) + if err != nil { + return err + } + structFieldValue.SetUint(uintVal) + } case reflect.Float32, reflect.Float64: - structFieldValue.SetFloat(val.Float()) + if val.CanFloat() { + structFieldValue.SetFloat(val.Float()) + } else { + floatVal, err := strconv.ParseFloat(fmt.Sprintf("%v", val.Interface()), 64) + if err != nil { + return err + } + structFieldValue.SetFloat(floatVal) + } case reflect.Struct: m := val.Interface().(map[interface{}]interface{}) - fillStruct(structFieldValue, m) + return fillStruct(structFieldValue, m) case reflect.Slice: l := val.Len() + if l == 0 { + break + } + arrayOfObjects := reflect.MakeSlice(structFieldValue.Type(), l, l) for i := 0; i < l; i++ { if m, ok := val.Index(i).Interface().(map[interface{}]interface{}); ok { obj := arrayOfObjects.Index(i) - fillStruct(obj, m) + if obj.Kind() == reflect.Ptr { + obj.Set(reflect.New(obj.Type().Elem())) + obj = obj.Elem() + } + if err := setField(obj, m); err != nil { + return err + } } else { - switch arrayOfObjects.Index(i).Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - arrayOfObjects.Index(i).SetInt(val.Index(i).Elem().Int()) - case reflect.Float32, reflect.Float64: - arrayOfObjects.Index(i).SetFloat(val.Index(i).Elem().Float()) - default: - arrayOfObjects.Index(i).Set(val.Index(i).Elem()) + if err := setField(arrayOfObjects.Index(i), val.Index(i).Interface()); err != nil { + return err } - } } structFieldValue.Set(arrayOfObjects) + + case reflect.Map: + l := val.Len() + if l == 0 { + break + } + + mapType := structFieldValue.Type() + + mapOfObjects := reflect.MakeMapWithSize(mapType, l) + + // Go randomises maps. To be able to test this we need to make sure the + // map keys always come out in the same order. So we sort them first. + mapKeys := val.MapKeys() + sort.Slice(mapKeys, func(i, j int) bool { + return lessValue(mapKeys[i], mapKeys[j]) + }) + + for _, k := range mapKeys { + kValue := reflect.New(mapType.Key()).Elem() + if err := setField(kValue, k.Interface()); err != nil { + return err + } + + v := val.MapIndex(k) + vValue := reflect.New(mapType.Elem()).Elem() + if err := setField(vValue, v.Interface()); err != nil { + return err + } + + mapOfObjects.SetMapIndex(kValue, vValue) + } + + structFieldValue.Set(mapOfObjects) + case reflect.Ptr: // Instantiate structFieldValue. structFieldValue.Set(reflect.New(structFieldValue.Type().Elem())) return setField(structFieldValue.Elem(), value) + + case reflect.String: + var str string + if val.CanInterface() { + str = fmt.Sprintf("%v", val.Interface()) + } else { + str = val.String() + } + structFieldValue.SetString(str) + + case reflect.Bool: + structFieldValue.SetBool(val.Bool()) + default: structFieldValue.Set(val) } @@ -235,16 +313,36 @@ func fillStruct(obj reflect.Value, m map[interface{}]interface{}) error { if !field.CanSet() { continue } - var key string - if tag := tt.Field(i).Tag.Get("php"); tag == "-" { + + fieldType := tt.Field(i) + + if fieldType.Anonymous { + // embedded struct + if err := setField(field, m); err != nil { + return err + } + continue + } + tag := fieldType.Tag.Get("php") + if v, ok := m[tag]; tag != "" && ok { + if err := setField(field, v); err != nil { + return err + } + continue + } + fieldName := fieldType.Name + lowerCaseFieldName := lowerCaseFirstLetter(fieldName) + if v, ok := m[lowerCaseFieldName]; ok { + if err := setField(field, v); err != nil { + return err + } continue - } else if tag != "" { - key = tag - } else { - key = lowerCaseFirstLetter(tt.Field(i).Name) } - if v, ok := m[key]; ok { - setField(field, v) + if v, ok := m[fieldName]; ok { + if err := setField(field, v); err != nil { + return err + } + continue } } @@ -340,6 +438,15 @@ func consumeAssociativeArray(data []byte, offset int) (map[interface{}]interface return result, offset + 1, nil } +func consumeAssociativeArrayIntoStruct(data []byte, offset int, v reflect.Value) (int, error) { + m, offset, err := consumeAssociativeArray(data, offset) + if err != nil { + return -1, err + } + + return offset, fillStruct(v, stringifyKeys(m).(map[interface{}]interface{})) +} + func consumeIndexedArray(data []byte, offset int) ([]interface{}, int, error) { if !checkType(data, 'a', offset) { return []interface{}{}, -1, errors.New("not an array") @@ -381,3 +488,60 @@ func consumeIndexedArray(data []byte, offset int) ([]interface{}, int, error) { // The +1 is for the final '}' return result, offset + 1, nil } + +func consumeIndexedArrayIntoStruct(data []byte, offset int, v reflect.Value) (int, error) { + s, offset, err := consumeIndexedArray(data, offset) + if err != nil { + return -1, err + } + + s = stringifyKeys(s).([]interface{}) + + l := len(s) + arrayOfObjects := reflect.MakeSlice(v.Type(), l, l) + + for i := 0; i < l; i++ { + if m, ok := s[i].(map[interface{}]interface{}); ok { + obj := arrayOfObjects.Index(i) + if obj.Kind() == reflect.Ptr { + obj.Set(reflect.New(obj.Type().Elem())) + obj = obj.Elem() + } + if err := setField(obj, m); err != nil { + return -1, err + } + } else { + if err := setField(arrayOfObjects.Index(i), s[i]); err != nil { + return -1, err + } + } + } + + v.Set(arrayOfObjects) + + return offset, nil +} + +// stringifyKeys recursively casts map keys into a real string but +// still stored as type interface{}, so the output still can be used +// in fillStruct() because it assumes a map[interface{}]interface{}. +func stringifyKeys(in interface{}) interface{} { + switch x := in.(type) { + case []interface{}: + newSlice := make([]interface{}, len(x)) + for i, v := range x { + newSlice[i] = stringifyKeys(v) + } + return newSlice + + case map[interface{}]interface{}: + newMap := map[interface{}]interface{}{} + for k, v := range x { + newMap[fmt.Sprintf("%v", k)] = stringifyKeys(v) + } + return newMap + + default: + return in + } +} diff --git a/unserialize.go b/unserialize.go index add62d9..00b961d 100644 --- a/unserialize.go +++ b/unserialize.go @@ -118,6 +118,11 @@ func UnmarshalAssociativeArray(data []byte) (map[interface{}]interface{}, error) } func UnmarshalObject(data []byte, v reflect.Value) error { + if checkType(data, 'a', 0) { + _, err := consumeAssociativeArrayIntoStruct(data, 0, v) + return err + } + _, err := consumeObject(data, 0, v) return err } @@ -167,9 +172,11 @@ func Unmarshal(data []byte, v interface{}) error { value.SetString(v) case reflect.Slice: + elemType := value.Type().Elem() + // uint8 is an alias for byte. This means we are trying to pull // a binary string out. - if value.Type().Elem().Kind() == reflect.Uint8 { + if elemType.Kind() == reflect.Uint8 { v, err := UnmarshalBytes(data) if err != nil { return err @@ -179,6 +186,16 @@ func Unmarshal(data []byte, v interface{}) error { return nil } + if elemType.Kind() == reflect.Struct || + (elemType.Kind() == reflect.Ptr && elemType.Elem().Kind() == reflect.Struct) { + _, err := consumeIndexedArrayIntoStruct(data, 0, value) + if err != nil { + return err + } + + return nil + } + // Otherwise this must be a slice (array) v, err := UnmarshalIndexedArray(data) if err != nil { diff --git a/unserialize_test.go b/unserialize_test.go index 9642e0e..d973604 100644 --- a/unserialize_test.go +++ b/unserialize_test.go @@ -456,6 +456,177 @@ func TestUnmarshalAssociativeArray(t *testing.T) { } } +func TestUnmarshalAssociativeArrayIntoStruct(t *testing.T) { + type Struct1 struct { + Foo int `php:"foo"` + Bar float64 + } + + type Struct2 struct { + One int64 `php:"0"` + Two string `php:"1"` + } + + type FooBar struct { + Foo uint64 `php:"foo"` + Bar int64 `php:"bar"` + } + + type Struct3 struct { + FooBar + FooBarField FooBar `php:"foobar"` + } + + type Struct4 struct { + Uints []uint64 + FooBars []FooBar `php:"foobars"` + Any []interface{} + } + + type Struct5 struct { + Foo *int `php:"foo"` + Bar *float64 + } + var foo int = 10 + var bar float64 = 20 + + type Struct6 struct { + One *int64 `php:"0"` + Two *string `php:"1"` + } + var one int64 = 10 + var two string = "foo" + + type Struct7 struct { + *FooBar + FooBarField *FooBar `php:"foobar"` + } + + type Struct8 struct { + FooBars1 []*FooBar + FooBars2 *[]FooBar + FooBars3 *[]*FooBar + } + + type AString string + type ABool bool + type Binary []byte + type Strings []AString + type Permissions map[string]Strings + type Struct9 struct { + String AString + Bool ABool + Binary Binary + Permissions Permissions + } + + type Struct10 struct { + Map map[string]string + } + + tests := map[string]struct { + input []byte + result interface{} + expectedOutput interface{} + expectedError error + }{ + `Simple struct`: { + []byte(`a:2:{s:3:"Bar";i:20;s:3:"foo";d:10;}`), + &Struct1{}, + Struct1{Foo: 10, Bar: 20.0}, + nil, + }, + `Indexed list`: { + []byte(`a:2:{i:0;i:10;i:1;s:3:"foo";}`), + &Struct2{}, + Struct2{One: 10, Two: "foo"}, + nil, + }, + `Structs in struct`: { + []byte(`a:3:{s:3:"foo";i:10;s:3:"bar";i:20;s:6:"foobar";a:2:{s:3:"foo";i:30;s:3:"bar";i:40;}}`), + &Struct3{}, + Struct3{FooBar: FooBar{Foo: 10, Bar: 20}, FooBarField: FooBar{Foo: 30, Bar: 40}}, + nil, + }, + `Slices in struct`: { + []byte(`a:3:{s:7:"foobars";a:2:{i:0;a:2:{s:3:"foo";i:30;s:3:"bar";i:40;}i:1;a:2:{s:3:"foo";i:50;s:3:"bar";i:60;}}s:5:"Uints";a:2:{i:0;i:10;i:1;i:20;}s:3:"Any";a:2:{i:0;i:30;i:1;s:5:"forty";}}`), + &Struct4{}, + Struct4{Uints: []uint64{10, 20}, FooBars: []FooBar{{Foo: 30, Bar: 40}, {Foo: 50, Bar: 60}}, Any: []interface{}{int64(30), "forty"}}, + nil, + }, + `Not an object`: { + []byte(`N;`), + &Struct1{}, + nil, + errors.New("not an object"), + }, + `Simple struct with pointers`: { + []byte(`a:2:{s:3:"Bar";i:20;s:3:"foo";d:10;}`), + &Struct5{}, + Struct5{Foo: &foo, Bar: &bar}, + nil, + }, + `Indexed list with pointers`: { + []byte(`a:2:{i:0;i:10;i:1;s:3:"foo";}`), + &Struct6{}, + Struct6{One: &one, Two: &two}, + nil, + }, + `Structs in struct with pointers`: { + []byte(`a:3:{s:3:"foo";i:10;s:3:"bar";i:20;s:6:"foobar";a:2:{s:3:"foo";i:30;s:3:"bar";i:40;}}`), + &Struct7{}, + Struct7{FooBar: &FooBar{Foo: 10, Bar: 20}, FooBarField: &FooBar{Foo: 30, Bar: 40}}, + nil, + }, + `Slices in struct with pointers`: { + []byte(`a:3:{s:8:"FooBars1";a:2:{i:0;a:2:{s:3:"foo";i:10;s:3:"bar";i:20;}i:1;a:2:{s:3:"foo";i:30;s:3:"bar";i:40;}}s:8:"FooBars2";a:2:{i:0;a:2:{s:3:"foo";i:50;s:3:"bar";i:60;}i:1;a:2:{s:3:"foo";i:70;s:3:"bar";i:80;}}s:8:"FooBars3";a:2:{i:0;a:2:{s:3:"foo";i:90;s:3:"bar";i:100;}i:1;a:2:{s:3:"foo";i:110;s:3:"bar";i:120;}}}`), + &Struct8{}, + Struct8{ + FooBars1: []*FooBar{{Foo: 10, Bar: 20}, {Foo: 30, Bar: 40}}, + FooBars2: &[]FooBar{{Foo: 50, Bar: 60}, {Foo: 70, Bar: 80}}, + FooBars3: &[]*FooBar{{Foo: 90, Bar: 100}, {Foo: 110, Bar: 120}}, + }, + nil, + }, + `Type alias`: { + []byte("a:4:{s:6:\"String\";s:3:\"foo\";s:4:\"Bool\";b:1;s:6:\"Binary\";s:3:\"\x01\x02\x03\";s:11:\"Permissions\";a:1:{s:1:\"k\";a:2:{i:0;s:2:\"v1\";i:1;s:2:\"v2\";}}}"), + &Struct9{}, + Struct9{ + String: "foo", + Bool: true, + Binary: []byte{1, 2, 3}, + Permissions: map[string]Strings{"k": {"v1", "v2"}}, + }, + nil, + }, + `Map in struct`: { + []byte("a:1:{s:3:\"Map\";a:1:{s:3:\"foo\";s:3:\"bar\";}}"), + &Struct10{}, + Struct10{ + Map: map[string]string{"foo": "bar"}, + }, + nil, + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + + err := phpserialize.Unmarshal(test.input, test.result) + + if test.expectedError == nil { + expectErrorToNotHaveOccurred(t, err) + result := reflect.ValueOf(test.result).Elem().Interface() + if !reflect.DeepEqual(result, test.expectedOutput) { + t.Errorf("Expected %+v, got %+v", test.expectedOutput, result) + } + } else { + expectErrorToEqual(t, err, test.expectedError) + } + }) + } +} + var inputNull = []byte("N;") var inputBoolFalse = []byte("b:0;") var inputBoolTrue = []byte("b:1;") @@ -661,6 +832,52 @@ func TestUnmarshalArrayThatContainsObject(t *testing.T) { } } +func TestUnmarshalArrayToSliceOfStructs(t *testing.T) { + type struct1 struct { + Foo int `php:"foo"` + Bar *int `php:"bar"` + } + + data := `a:3:{i:0;a:2:{s:3:"foo";i:1;s:3:"bar";i:2;}i:1;a:2:{s:3:"foo";i:3;s:3:"bar";i:4;}i:2;a:2:{s:3:"foo";i:5;s:3:"bar";i:6;}}` + var result []struct1 + err := phpserialize.Unmarshal([]byte(data), &result) + expectErrorToNotHaveOccurred(t, err) + + expected := []struct1{ + {Foo: 1, Bar: new(int)}, + {Foo: 3, Bar: new(int)}, + {Foo: 5, Bar: new(int)}, + } + *expected[0].Bar = 2 + *expected[1].Bar = 4 + *expected[2].Bar = 6 + + if !reflect.DeepEqual(result, expected) { + t.Errorf("Expected:\n %#+v\nGot:\n %#+v", expected, result) + } +} + +func TestUnmarshalArrayToSliceOfStructPtrs(t *testing.T) { + type struct1 struct { + Foo int `php:"foo"` + } + + data := `a:3:{i:0;a:1:{s:3:"foo";i:1;}i:1;a:1:{s:3:"foo";i:2;}i:2;a:1:{s:3:"foo";i:3;}}` + var result []*struct1 + err := phpserialize.Unmarshal([]byte(data), &result) + expectErrorToNotHaveOccurred(t, err) + + expected := []*struct1{ + {Foo: 1}, + {Foo: 2}, + {Foo: 3}, + } + + if !reflect.DeepEqual(result, expected) { + t.Errorf("Expected:\n %#+v\nGot:\n %#+v", expected, result) + } +} + // https://github.com/elliotchance/phpserialize/issues/7 func TestUnmarshalArrayThatContainsInteger(t *testing.T) { data := `a:3:{s:4:"name";s:2:"tw";s:3:"age";i:123;s:4:"wife";a:1:{s:1:"x";s:1:"y";}}` From 0bc59da980a57cb369c4b0a31d3d4a9f59723a52 Mon Sep 17 00:00:00 2001 From: Yehezkiel Syamsuhadi Date: Mon, 13 Apr 2026 12:43:56 +1000 Subject: [PATCH 3/3] fix marshaling embeded struct as a map --- serialize.go | 60 +++++++++++++++++++++++++++++++++++------------ serialize_test.go | 38 +++++++++++++++++++++++++++--- 2 files changed, 80 insertions(+), 18 deletions(-) diff --git a/serialize.go b/serialize.go index e6fbea0..1895952 100644 --- a/serialize.go +++ b/serialize.go @@ -145,6 +145,11 @@ func MarshalNil() []byte { // name are maintained. At the moment there is no way to change this behaviour, // unlike other marshallers that use a tag on the field. func MarshalStruct(input interface{}, options *MarshalOptions) ([]byte, error) { + _, m, err := marshalStruct(input, options, false) + return m, err +} + +func marshalStruct(input interface{}, options *MarshalOptions, embeded bool) (int, []byte, error) { value := reflect.ValueOf(input) typeOfValue := value.Type() @@ -155,44 +160,69 @@ func MarshalStruct(input interface{}, options *MarshalOptions) ([]byte, error) { var buffer bytes.Buffer for i := 0; i < value.NumField(); i++ { f := value.Field(i) + ft := typeOfValue.Field(i) if !f.CanInterface() { // This is an unexported field, we cannot read it. continue } - visibleFieldCount++ + fieldName, fieldOptions := parseTag(ft.Tag.Get("php")) + if fieldOptions.Contains("omitnilptr") && f.Kind() == reflect.Ptr && f.IsNil() { + continue + } + if fieldName == "-" { + continue + } - fieldName, fieldOptions := parseTag(typeOfValue.Field(i).Tag.Get("php")) + if ft.Anonymous && options.MarshalStructAsMap { + if f.Kind() == reflect.Struct { + // the field is embedded struct + fields, m, err := marshalStruct(f.Interface(), options, true) + if err != nil { + return -1, nil, err + } + buffer.Write(m) + visibleFieldCount += fields + continue + } - if fieldOptions.Contains("omitnilptr") { - if f.Kind() == reflect.Ptr && f.IsNil() { - visibleFieldCount-- + if f.Kind() == reflect.Ptr && f.Elem().Kind() == reflect.Struct { + // the field is embedded struct ptr + fields, m, err := marshalStruct(f.Elem().Interface(), options, true) + if err != nil { + return -1, nil, err + } + buffer.Write(m) + visibleFieldCount += fields continue } } - if fieldName == "-" { - visibleFieldCount-- - continue - } else if fieldName == "" { - fieldName = typeOfValue.Field(i).Name + visibleFieldCount++ + + if fieldName == "" { + fieldName = ft.Name if !options.MarshalStructAsMap { fieldName = lowerCaseFirstLetter(fieldName) } } + buffer.Write(MarshalString(fieldName)) m, err := Marshal(f.Interface(), options) if err != nil { - return nil, err + return -1, nil, err } buffer.Write(m) } if options.MarshalStructAsMap { - return []byte(fmt.Sprintf("a:%d:{%s}", visibleFieldCount, buffer.String())), nil + if embeded { + return visibleFieldCount, buffer.Bytes(), nil + } + return visibleFieldCount, []byte(fmt.Sprintf("a:%d:{%s}", visibleFieldCount, buffer.String())), nil } className := reflect.ValueOf(input).Type().Name() @@ -200,14 +230,14 @@ func MarshalStruct(input interface{}, options *MarshalOptions) ([]byte, error) { className = "stdClass" } - return []byte(fmt.Sprintf("O:%d:\"%s\":%d:{%s}", len(className), - className, visibleFieldCount, buffer.String())), nil + return visibleFieldCount, + []byte(fmt.Sprintf("O:%d:\"%s\":%d:{%s}", len(className), className, visibleFieldCount, buffer.String())), + nil } // Marshal is the canonical way to perform the equivalent of serialize() in PHP. // It can handle encoding scalar types, slices and maps. func Marshal(input interface{}, options *MarshalOptions) ([]byte, error) { - if options == nil { options = DefaultMarshalOptions() } diff --git a/serialize_test.go b/serialize_test.go index 05b3b05..8b1fba2 100644 --- a/serialize_test.go +++ b/serialize_test.go @@ -6,9 +6,7 @@ import ( "testing" ) -var ( - heyStr = "hey" -) +var heyStr = "hey" type struct1 struct { Foo int @@ -44,6 +42,30 @@ type Nillable struct { BarPtr *Struct2 } +type Foo struct { + Foo int +} + +type Bar struct { + Foo + Bar int +} + +type WithEmbed struct { + Bar + Baz int +} + +type BarPtr struct { + *Foo + Bar int +} + +type WithEmbedPtr struct { + *BarPtr + Baz int +} + type marshalTest struct { input interface{} output []byte @@ -169,6 +191,16 @@ var marshalTests = map[string]marshalTest{ []byte("a:3:{s:3:\"Foo\";i:20;s:3:\"Bar\";a:1:{s:3:\"Qux\";d:7.89;}s:3:\"Baz\";s:3:\"yay\";}"), getMarshalStructAsMap(), }, + "WithEmbed{Bar: Bar{Foo: Foo{Foo:10}, Bar:20}, Baz: 30} as map": { + WithEmbed{Bar: Bar{Foo: Foo{Foo: 10}, Bar: 20}, Baz: 30}, + []byte("a:3:{s:3:\"Foo\";i:10;s:3:\"Bar\";i:20;s:3:\"Baz\";i:30;}"), + getMarshalStructAsMap(), + }, + "WithEmbedPtr{Embeded: &Embeded{Foo: 10}, Bar: 20} as map": { + WithEmbedPtr{BarPtr: &BarPtr{Foo: &Foo{Foo: 10}, Bar: 20}, Baz: 30}, + []byte("a:3:{s:3:\"Foo\";i:10;s:3:\"Bar\";i:20;s:3:\"Baz\";i:30;}"), + getMarshalStructAsMap(), + }, // encode object with array of objects "struct3{ObjectArray Struct2{Qux float64}, IntArray {1, 2}, FloatArray {1.0, 2.0}, StringArray {'a', 'b'}}": {