From 0bfd2ea7a0be8f609d87bef4620758b225d22d86 Mon Sep 17 00:00:00 2001 From: Stephan Kim Date: Fri, 3 Feb 2017 17:53:23 -0800 Subject: [PATCH 1/5] working version --- constants.go | 1 + node.go | 32 ++++++ request.go | 18 +++- response.go | 29 ++++++ response_embedded_test.go | 205 ++++++++++++++++++++++++++++++++++++++ response_test.go | 2 +- 6 files changed, 284 insertions(+), 3 deletions(-) create mode 100644 response_embedded_test.go diff --git a/constants.go b/constants.go index 23288d31..d5cd0ea5 100644 --- a/constants.go +++ b/constants.go @@ -10,6 +10,7 @@ const ( annotationOmitEmpty = "omitempty" annotationISO8601 = "iso8601" annotationSeperator = "," + annotationIgnore = "-" iso8601TimeFormat = "2006-01-02T15:04:05Z" diff --git a/node.go b/node.go index 3a0c02e2..a472347d 100644 --- a/node.go +++ b/node.go @@ -28,6 +28,38 @@ type Node struct { Links *Links `json:"links,omitempty"` } +func (n *Node) merge(node *Node) { + if node.Type != "" { + n.Type = node.Type + } + + if node.ID != "" { + n.ID = node.ID + } + + if node.ClientID != "" { + n.ClientID = node.ClientID + } + + if n.Attributes == nil && node.Attributes != nil { + n.Attributes = make(map[string]interface{}) + } + for k, v := range node.Attributes { + n.Attributes[k] = v + } + + if n.Relationships == nil && n.Relationships != nil { + n.Relationships = make(map[string]interface{}) + } + for k, v := range node.Relationships { + n.Relationships[k] = v + } + + if node.Links != nil { + n.Links = node.Links + } +} + // RelationshipOneNode is used to represent a generic has one JSON API relation type RelationshipOneNode struct { Data *Node `json:"data"` diff --git a/request.go b/request.go index 335ecb42..642291ae 100644 --- a/request.go +++ b/request.go @@ -131,14 +131,28 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) for i := 0; i < modelValue.NumField(); i++ { fieldType := modelType.Field(i) - tag := fieldType.Tag.Get("jsonapi") + tag := fieldType.Tag.Get(annotationJSONAPI) + + // handles embedded structs + if isEmbeddedStruct(fieldType) { + if shouldIgnoreField(tag) { + continue + } + model := reflect.ValueOf(modelValue.Field(i).Addr().Interface()) + err := unmarshalNode(data, model, included) + if err != nil { + er = err + break + } + } + if tag == "" { continue } fieldValue := modelValue.Field(i) - args := strings.Split(tag, ",") + args := strings.Split(tag, annotationSeperator) if len(args) < 1 { er = ErrBadJSONAPIStructTag diff --git a/response.go b/response.go index c44cd3b3..7c9642a4 100644 --- a/response.go +++ b/response.go @@ -199,6 +199,20 @@ func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error { return nil } +func isEmbeddedStruct(sField reflect.StructField) bool { + if sField.Anonymous && sField.Type.Kind() == reflect.Struct { + return true + } + return false +} + +func shouldIgnoreField(japiTag string) bool { + if strings.HasPrefix(japiTag, annotationIgnore) { + return true + } + return false +} + func visitModelNode(model interface{}, included *map[string]*Node, sideload bool) (*Node, error) { node := new(Node) @@ -211,6 +225,21 @@ func visitModelNode(model interface{}, included *map[string]*Node, for i := 0; i < modelValue.NumField(); i++ { structField := modelValue.Type().Field(i) tag := structField.Tag.Get(annotationJSONAPI) + + // handles embedded structs + if isEmbeddedStruct(structField) { + if shouldIgnoreField(tag) { + continue + } + model := modelValue.Field(i).Addr().Interface() + embNode, err := visitModelNode(model, included, sideload) + if err != nil { + er = err + break + } + node.merge(embNode) + } + if tag == "" { continue } diff --git a/response_embedded_test.go b/response_embedded_test.go new file mode 100644 index 00000000..51e3bf05 --- /dev/null +++ b/response_embedded_test.go @@ -0,0 +1,205 @@ +package jsonapi + +import ( + "bytes" + "reflect" + "testing" +) + +func TestMergeNode(t *testing.T) { + parent := &Node{ + Type: "Good", + ID: "99", + Attributes: map[string]interface{}{"fizz": "buzz"}, + } + + child := &Node{ + Type: "Better", + ClientID: "1111", + Attributes: map[string]interface{}{"timbuk": 2}, + } + + expected := &Node{ + Type: "Better", + ID: "99", + ClientID: "1111", + Attributes: map[string]interface{}{"fizz": "buzz", "timbuk": 2}, + } + + parent.merge(child) + + if !reflect.DeepEqual(expected, parent) { + t.Errorf("Got %+v Expected %+v", parent, expected) + } +} + +func TestIsEmbeddedStruct(t *testing.T) { + type foo struct{} + + structType := reflect.TypeOf(foo{}) + stringType := reflect.TypeOf("") + if structType.Kind() != reflect.Struct { + t.Fatal("structType.Kind() is not a struct.") + } + if stringType.Kind() != reflect.String { + t.Fatal("stringType.Kind() is not a string.") + } + + type test struct { + scenario string + input reflect.StructField + expectedRes bool + } + + tests := []test{ + test{ + scenario: "success", + input: reflect.StructField{Anonymous: true, Type: structType}, + expectedRes: true, + }, + test{ + scenario: "wrong type", + input: reflect.StructField{Anonymous: true, Type: stringType}, + expectedRes: false, + }, + test{ + scenario: "not embedded", + input: reflect.StructField{Type: structType}, + expectedRes: false, + }, + } + + for _, test := range tests { + res := isEmbeddedStruct(test.input) + if res != test.expectedRes { + t.Errorf("Scenario -> %s\nGot -> %v\nExpected -> %v\n", test.scenario, res, test.expectedRes) + } + } +} + +func TestShouldIgnoreField(t *testing.T) { + type test struct { + scenario string + input string + expectedRes bool + } + + tests := []test{ + test{ + scenario: "opt-out", + input: annotationIgnore, + expectedRes: true, + }, + test{ + scenario: "no tag", + input: "", + expectedRes: false, + }, + test{ + scenario: "wrong tag", + input: "wrong,tag", + expectedRes: false, + }, + } + + for _, test := range tests { + res := shouldIgnoreField(test.input) + if res != test.expectedRes { + t.Errorf("Scenario -> %s\nGot -> %v\nExpected -> %v\n", test.scenario, res, test.expectedRes) + } + } +} + +func TestIsValidEmbeddedStruct(t *testing.T) { + type foo struct{} + + structType := reflect.TypeOf(foo{}) + stringType := reflect.TypeOf("") + if structType.Kind() != reflect.Struct { + t.Fatal("structType.Kind() is not a struct.") + } + if stringType.Kind() != reflect.String { + t.Fatal("stringType.Kind() is not a string.") + } + + type test struct { + scenario string + input reflect.StructField + expectedRes bool + } + + tests := []test{ + test{ + scenario: "success", + input: reflect.StructField{Anonymous: true, Type: structType}, + expectedRes: true, + }, + test{ + scenario: "opt-out", + input: reflect.StructField{Anonymous: true, Tag: "jsonapi:\"-\"", Type: structType}, + expectedRes: false, + }, + test{ + scenario: "wrong type", + input: reflect.StructField{Anonymous: true, Type: stringType}, + expectedRes: false, + }, + test{ + scenario: "not embedded", + input: reflect.StructField{Type: structType}, + expectedRes: false, + }, + } + + for _, test := range tests { + res := (isEmbeddedStruct(test.input) && !shouldIgnoreField(test.input.Tag.Get(annotationJSONAPI))) + if res != test.expectedRes { + t.Errorf("Scenario -> %s\nGot -> %v\nExpected -> %v\n", test.scenario, res, test.expectedRes) + } + } +} + +func TestMarshalUnmarshalCompositeStruct(t *testing.T) { + type Thing struct { + ID int `jsonapi:"primary,things"` + Fizz string `jsonapi:"attr,fizz"` + Buzz int `jsonapi:"attr,buzz"` + } + + type Model struct { + Thing + Foo string `jsonapi:"attr,foo"` + Bar string `jsonapi:"attr,bar"` + Bat string `jsonapi:"attr,bat"` + } + + model := &Model{} + model.ID = 1 + model.Fizz = "fizzy" + model.Buzz = 99 + model.Foo = "fooey" + model.Bar = "barry" + model.Bat = "batty" + + buf := bytes.NewBuffer(nil) + if err := MarshalOnePayload(buf, model); err != nil { + t.Fatal(err) + } + + // TODO: redo this + // assert encoding from model to jsonapi output + // expected := `{"data":{"type":"things","id":"1","attributes":{"bar":"barry","bat":"batty","buzz":99,"fizz":"fizzy","foo":"fooey"}}}` + // if expected != string(buf.Bytes()) { + // t.Errorf("Got %+v Expected %+v", string(buf.Bytes()), expected) + // } + + dst := &Model{} + if err := UnmarshalPayload(buf, dst); err != nil { + t.Fatal(err) + } + + // assert decoding from jsonapi output to model + if !reflect.DeepEqual(model, dst) { + t.Errorf("Got %#v Expected %#v", dst, model) + } +} diff --git a/response_test.go b/response_test.go index 756fe872..c1edbf39 100644 --- a/response_test.go +++ b/response_test.go @@ -59,7 +59,7 @@ func (b *Blog) JSONAPIRelationshipLinks(relation string) *Links { } type Post struct { - Blog + Blog `jsonapi:"-"` ID uint64 `jsonapi:"primary,posts"` BlogID int `jsonapi:"attr,blog_id"` ClientID string `jsonapi:"client-id"` From 8e20fb25dec7d448b66bbfa679f80e7d4c491405 Mon Sep 17 00:00:00 2001 From: Stephan Kim Date: Mon, 6 Feb 2017 09:42:47 -0800 Subject: [PATCH 2/5] fix text --- response_embedded_test.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/response_embedded_test.go b/response_embedded_test.go index 51e3bf05..0d997ca4 100644 --- a/response_embedded_test.go +++ b/response_embedded_test.go @@ -3,6 +3,7 @@ package jsonapi import ( "bytes" "reflect" + "strings" "testing" ) @@ -186,12 +187,13 @@ func TestMarshalUnmarshalCompositeStruct(t *testing.T) { t.Fatal(err) } - // TODO: redo this // assert encoding from model to jsonapi output - // expected := `{"data":{"type":"things","id":"1","attributes":{"bar":"barry","bat":"batty","buzz":99,"fizz":"fizzy","foo":"fooey"}}}` - // if expected != string(buf.Bytes()) { - // t.Errorf("Got %+v Expected %+v", string(buf.Bytes()), expected) - // } + expected := `{"data":{"type":"things","id":"1","attributes":{"bar":"barry","bat":"batty","buzz":99,"fizz":"fizzy","foo":"fooey"}}}` + actual := strings.TrimSpace(string(buf.Bytes())) + + if expected != actual { + t.Errorf("Got %+v Expected %+v", actual, expected) + } dst := &Model{} if err := UnmarshalPayload(buf, dst); err != nil { From e6596dc0ccf9d3e8a7c9a5b411b0cd57e4f44543 Mon Sep 17 00:00:00 2001 From: Stephan Kim Date: Mon, 6 Feb 2017 10:15:32 -0800 Subject: [PATCH 3/5] combine test files --- response_embedded_test.go | 207 -------------------------------------- response_test.go | 200 ++++++++++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+), 207 deletions(-) delete mode 100644 response_embedded_test.go diff --git a/response_embedded_test.go b/response_embedded_test.go deleted file mode 100644 index 0d997ca4..00000000 --- a/response_embedded_test.go +++ /dev/null @@ -1,207 +0,0 @@ -package jsonapi - -import ( - "bytes" - "reflect" - "strings" - "testing" -) - -func TestMergeNode(t *testing.T) { - parent := &Node{ - Type: "Good", - ID: "99", - Attributes: map[string]interface{}{"fizz": "buzz"}, - } - - child := &Node{ - Type: "Better", - ClientID: "1111", - Attributes: map[string]interface{}{"timbuk": 2}, - } - - expected := &Node{ - Type: "Better", - ID: "99", - ClientID: "1111", - Attributes: map[string]interface{}{"fizz": "buzz", "timbuk": 2}, - } - - parent.merge(child) - - if !reflect.DeepEqual(expected, parent) { - t.Errorf("Got %+v Expected %+v", parent, expected) - } -} - -func TestIsEmbeddedStruct(t *testing.T) { - type foo struct{} - - structType := reflect.TypeOf(foo{}) - stringType := reflect.TypeOf("") - if structType.Kind() != reflect.Struct { - t.Fatal("structType.Kind() is not a struct.") - } - if stringType.Kind() != reflect.String { - t.Fatal("stringType.Kind() is not a string.") - } - - type test struct { - scenario string - input reflect.StructField - expectedRes bool - } - - tests := []test{ - test{ - scenario: "success", - input: reflect.StructField{Anonymous: true, Type: structType}, - expectedRes: true, - }, - test{ - scenario: "wrong type", - input: reflect.StructField{Anonymous: true, Type: stringType}, - expectedRes: false, - }, - test{ - scenario: "not embedded", - input: reflect.StructField{Type: structType}, - expectedRes: false, - }, - } - - for _, test := range tests { - res := isEmbeddedStruct(test.input) - if res != test.expectedRes { - t.Errorf("Scenario -> %s\nGot -> %v\nExpected -> %v\n", test.scenario, res, test.expectedRes) - } - } -} - -func TestShouldIgnoreField(t *testing.T) { - type test struct { - scenario string - input string - expectedRes bool - } - - tests := []test{ - test{ - scenario: "opt-out", - input: annotationIgnore, - expectedRes: true, - }, - test{ - scenario: "no tag", - input: "", - expectedRes: false, - }, - test{ - scenario: "wrong tag", - input: "wrong,tag", - expectedRes: false, - }, - } - - for _, test := range tests { - res := shouldIgnoreField(test.input) - if res != test.expectedRes { - t.Errorf("Scenario -> %s\nGot -> %v\nExpected -> %v\n", test.scenario, res, test.expectedRes) - } - } -} - -func TestIsValidEmbeddedStruct(t *testing.T) { - type foo struct{} - - structType := reflect.TypeOf(foo{}) - stringType := reflect.TypeOf("") - if structType.Kind() != reflect.Struct { - t.Fatal("structType.Kind() is not a struct.") - } - if stringType.Kind() != reflect.String { - t.Fatal("stringType.Kind() is not a string.") - } - - type test struct { - scenario string - input reflect.StructField - expectedRes bool - } - - tests := []test{ - test{ - scenario: "success", - input: reflect.StructField{Anonymous: true, Type: structType}, - expectedRes: true, - }, - test{ - scenario: "opt-out", - input: reflect.StructField{Anonymous: true, Tag: "jsonapi:\"-\"", Type: structType}, - expectedRes: false, - }, - test{ - scenario: "wrong type", - input: reflect.StructField{Anonymous: true, Type: stringType}, - expectedRes: false, - }, - test{ - scenario: "not embedded", - input: reflect.StructField{Type: structType}, - expectedRes: false, - }, - } - - for _, test := range tests { - res := (isEmbeddedStruct(test.input) && !shouldIgnoreField(test.input.Tag.Get(annotationJSONAPI))) - if res != test.expectedRes { - t.Errorf("Scenario -> %s\nGot -> %v\nExpected -> %v\n", test.scenario, res, test.expectedRes) - } - } -} - -func TestMarshalUnmarshalCompositeStruct(t *testing.T) { - type Thing struct { - ID int `jsonapi:"primary,things"` - Fizz string `jsonapi:"attr,fizz"` - Buzz int `jsonapi:"attr,buzz"` - } - - type Model struct { - Thing - Foo string `jsonapi:"attr,foo"` - Bar string `jsonapi:"attr,bar"` - Bat string `jsonapi:"attr,bat"` - } - - model := &Model{} - model.ID = 1 - model.Fizz = "fizzy" - model.Buzz = 99 - model.Foo = "fooey" - model.Bar = "barry" - model.Bat = "batty" - - buf := bytes.NewBuffer(nil) - if err := MarshalOnePayload(buf, model); err != nil { - t.Fatal(err) - } - - // assert encoding from model to jsonapi output - expected := `{"data":{"type":"things","id":"1","attributes":{"bar":"barry","bat":"batty","buzz":99,"fizz":"fizzy","foo":"fooey"}}}` - actual := strings.TrimSpace(string(buf.Bytes())) - - if expected != actual { - t.Errorf("Got %+v Expected %+v", actual, expected) - } - - dst := &Model{} - if err := UnmarshalPayload(buf, dst); err != nil { - t.Fatal(err) - } - - // assert decoding from jsonapi output to model - if !reflect.DeepEqual(model, dst) { - t.Errorf("Got %#v Expected %#v", dst, model) - } -} diff --git a/response_test.go b/response_test.go index c1edbf39..e7edd00f 100644 --- a/response_test.go +++ b/response_test.go @@ -6,6 +6,7 @@ import ( "fmt" "reflect" "sort" + "strings" "testing" "time" ) @@ -829,6 +830,205 @@ func TestMarshalMany_InvalidIntefaceArgument(t *testing.T) { } } +func TestMergeNode(t *testing.T) { + parent := &Node{ + Type: "Good", + ID: "99", + Attributes: map[string]interface{}{"fizz": "buzz"}, + } + + child := &Node{ + Type: "Better", + ClientID: "1111", + Attributes: map[string]interface{}{"timbuk": 2}, + } + + expected := &Node{ + Type: "Better", + ID: "99", + ClientID: "1111", + Attributes: map[string]interface{}{"fizz": "buzz", "timbuk": 2}, + } + + parent.merge(child) + + if !reflect.DeepEqual(expected, parent) { + t.Errorf("Got %+v Expected %+v", parent, expected) + } +} + +func TestIsEmbeddedStruct(t *testing.T) { + type foo struct{} + + structType := reflect.TypeOf(foo{}) + stringType := reflect.TypeOf("") + if structType.Kind() != reflect.Struct { + t.Fatal("structType.Kind() is not a struct.") + } + if stringType.Kind() != reflect.String { + t.Fatal("stringType.Kind() is not a string.") + } + + type test struct { + scenario string + input reflect.StructField + expectedRes bool + } + + tests := []test{ + test{ + scenario: "success", + input: reflect.StructField{Anonymous: true, Type: structType}, + expectedRes: true, + }, + test{ + scenario: "wrong type", + input: reflect.StructField{Anonymous: true, Type: stringType}, + expectedRes: false, + }, + test{ + scenario: "not embedded", + input: reflect.StructField{Type: structType}, + expectedRes: false, + }, + } + + for _, test := range tests { + res := isEmbeddedStruct(test.input) + if res != test.expectedRes { + t.Errorf("Scenario -> %s\nGot -> %v\nExpected -> %v\n", test.scenario, res, test.expectedRes) + } + } +} + +func TestShouldIgnoreField(t *testing.T) { + type test struct { + scenario string + input string + expectedRes bool + } + + tests := []test{ + test{ + scenario: "opt-out", + input: annotationIgnore, + expectedRes: true, + }, + test{ + scenario: "no tag", + input: "", + expectedRes: false, + }, + test{ + scenario: "wrong tag", + input: "wrong,tag", + expectedRes: false, + }, + } + + for _, test := range tests { + res := shouldIgnoreField(test.input) + if res != test.expectedRes { + t.Errorf("Scenario -> %s\nGot -> %v\nExpected -> %v\n", test.scenario, res, test.expectedRes) + } + } +} + +func TestIsValidEmbeddedStruct(t *testing.T) { + type foo struct{} + + structType := reflect.TypeOf(foo{}) + stringType := reflect.TypeOf("") + if structType.Kind() != reflect.Struct { + t.Fatal("structType.Kind() is not a struct.") + } + if stringType.Kind() != reflect.String { + t.Fatal("stringType.Kind() is not a string.") + } + + type test struct { + scenario string + input reflect.StructField + expectedRes bool + } + + tests := []test{ + test{ + scenario: "success", + input: reflect.StructField{Anonymous: true, Type: structType}, + expectedRes: true, + }, + test{ + scenario: "opt-out", + input: reflect.StructField{Anonymous: true, Tag: "jsonapi:\"-\"", Type: structType}, + expectedRes: false, + }, + test{ + scenario: "wrong type", + input: reflect.StructField{Anonymous: true, Type: stringType}, + expectedRes: false, + }, + test{ + scenario: "not embedded", + input: reflect.StructField{Type: structType}, + expectedRes: false, + }, + } + + for _, test := range tests { + res := (isEmbeddedStruct(test.input) && !shouldIgnoreField(test.input.Tag.Get(annotationJSONAPI))) + if res != test.expectedRes { + t.Errorf("Scenario -> %s\nGot -> %v\nExpected -> %v\n", test.scenario, res, test.expectedRes) + } + } +} + +func TestMarshalUnmarshalCompositeStruct(t *testing.T) { + type Thing struct { + ID int `jsonapi:"primary,things"` + Fizz string `jsonapi:"attr,fizz"` + Buzz int `jsonapi:"attr,buzz"` + } + + type Model struct { + Thing + Foo string `jsonapi:"attr,foo"` + Bar string `jsonapi:"attr,bar"` + Bat string `jsonapi:"attr,bat"` + } + + model := &Model{} + model.ID = 1 + model.Fizz = "fizzy" + model.Buzz = 99 + model.Foo = "fooey" + model.Bar = "barry" + model.Bat = "batty" + + buf := bytes.NewBuffer(nil) + if err := MarshalOnePayload(buf, model); err != nil { + t.Fatal(err) + } + + // assert encoding from model to jsonapi output + expected := `{"data":{"type":"things","id":"1","attributes":{"bar":"barry","bat":"batty","buzz":99,"fizz":"fizzy","foo":"fooey"}}}` + actual := strings.TrimSpace(string(buf.Bytes())) + + if expected != actual { + t.Errorf("Got %+v Expected %+v", actual, expected) + } + + dst := &Model{} + if err := UnmarshalPayload(buf, dst); err != nil { + t.Fatal(err) + } + + // assert decoding from jsonapi output to model + if !reflect.DeepEqual(model, dst) { + t.Errorf("Got %#v Expected %#v", dst, model) + } +} + func testBlog() *Blog { return &Blog{ ID: 5, From 5dbf82fac1a4b1a4d622448a1ab137a3d35e55d6 Mon Sep 17 00:00:00 2001 From: Stephan Kim Date: Mon, 6 Feb 2017 10:18:56 -0800 Subject: [PATCH 4/5] move private funcs to bottom --- response.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/response.go b/response.go index 7c9642a4..51ef1572 100644 --- a/response.go +++ b/response.go @@ -199,20 +199,6 @@ func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error { return nil } -func isEmbeddedStruct(sField reflect.StructField) bool { - if sField.Anonymous && sField.Type.Kind() == reflect.Struct { - return true - } - return false -} - -func shouldIgnoreField(japiTag string) bool { - if strings.HasPrefix(japiTag, annotationIgnore) { - return true - } - return false -} - func visitModelNode(model interface{}, included *map[string]*Node, sideload bool) (*Node, error) { node := new(Node) @@ -546,3 +532,17 @@ func convertToSliceInterface(i *interface{}) ([]interface{}, error) { } return response, nil } + +func isEmbeddedStruct(sField reflect.StructField) bool { + if sField.Anonymous && sField.Type.Kind() == reflect.Struct { + return true + } + return false +} + +func shouldIgnoreField(japiTag string) bool { + if strings.HasPrefix(japiTag, annotationIgnore) { + return true + } + return false +} From 86584179b22e0fa05879ba30a7bc670910a28b7a Mon Sep 17 00:00:00 2001 From: Stephan Kim Date: Mon, 20 Feb 2017 21:06:04 -0800 Subject: [PATCH 5/5] ErrInvalidType should ignore interfaces --- request.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/request.go b/request.go index 642291ae..33b9b091 100644 --- a/request.go +++ b/request.go @@ -460,7 +460,8 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) } // As a final catch-all, ensure types line up to avoid a runtime panic. - if fieldValue.Kind() != v.Kind() { + // Ignore interfaces since interfaces are poly + if fieldValue.Kind() != reflect.Interface && fieldValue.Kind() != v.Kind() { return ErrInvalidType } fieldValue.Set(reflect.ValueOf(val))