From e13a19922dbb23f2022f028e65b625043a6793b8 Mon Sep 17 00:00:00 2001 From: Slemgrim Date: Mon, 10 Jul 2017 22:09:01 +0200 Subject: [PATCH 01/17] extract type handling --- request.go | 301 +++++++++++++++++++++++++++-------------------------- 1 file changed, 153 insertions(+), 148 deletions(-) diff --git a/request.go b/request.go index ea6ae45..f879e1b 100644 --- a/request.go +++ b/request.go @@ -118,6 +118,7 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) { } func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) { + defer func() { if r := recover(); r != nil { err = fmt.Errorf("data is not a jsonapi representation of '%v'", model.Type()) @@ -248,122 +249,59 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) continue } - var iso8601 bool + var isIso8601 bool if len(args) > 2 { for _, arg := range args[2:] { if arg == annotationISO8601 { - iso8601 = true + isIso8601 = true } } } - val := attributes[args[1]] + attribute := attributes[args[1]] // continue if the attribute was not included in the request - if val == nil { + if attribute == nil { continue } - v := reflect.ValueOf(val) + value := reflect.ValueOf(attribute) + + // Handle field of type []string + if fieldValue.Type() == reflect.TypeOf([]string{}) { + values := handleStringSlice(value) + assign(fieldValue, reflect.ValueOf(values)) + continue + } // Handle field of type time.Time if fieldValue.Type() == reflect.TypeOf(time.Time{}) { - if iso8601 { - var tm string - if v.Kind() == reflect.String { - tm = v.Interface().(string) - } else { - er = ErrInvalidISO8601 - break - } - - t, err := time.Parse(iso8601TimeFormat, tm) - if err != nil { - er = ErrInvalidISO8601 - break - } - - fieldValue.Set(reflect.ValueOf(t)) - - continue + var time time.Time + if time, err = handleTime(value, isIso8601); err != nil { + er = err + break } - var at int64 - - if v.Kind() == reflect.Float64 { - at = int64(v.Interface().(float64)) - } else if v.Kind() == reflect.Int { - at = v.Int() - } else { - return ErrInvalidTime - } - - t := time.Unix(at, 0) - - fieldValue.Set(reflect.ValueOf(t)) - - continue - } - - if fieldValue.Type() == reflect.TypeOf([]string{}) { - values := make([]string, v.Len()) - for i := 0; i < v.Len(); i++ { - values[i] = v.Index(i).Interface().(string) - } - - fieldValue.Set(reflect.ValueOf(values)) - + assign(fieldValue, reflect.ValueOf(time)) continue } + // Handle field of type *time.Time if fieldValue.Type() == reflect.TypeOf(new(time.Time)) { - if iso8601 { - var tm string - if v.Kind() == reflect.String { - tm = v.Interface().(string) - } else { - er = ErrInvalidISO8601 - break - } - - v, err := time.Parse(iso8601TimeFormat, tm) - if err != nil { - er = ErrInvalidISO8601 - break - } - - t := &v - - fieldValue.Set(reflect.ValueOf(t)) - - continue + var time time.Time + if time, err = handleTime(value, isIso8601); err != nil { + er = err + break } - var at int64 - - if v.Kind() == reflect.Float64 { - at = int64(v.Interface().(float64)) - } else if v.Kind() == reflect.Int { - at = v.Int() - } else { - return ErrInvalidTime - } - - v := time.Unix(at, 0) - t := &v - - fieldValue.Set(reflect.ValueOf(t)) - + assign(fieldValue, reflect.ValueOf(&time)) continue } // JSON value was a float (numeric) - if v.Kind() == reflect.Float64 { - floatValue := v.Interface().(float64) + if value.Kind() == reflect.Float64 { - // The field may or may not be a pointer to a numeric; the kind var - // will not contain a pointer type var kind reflect.Kind if fieldValue.Kind() == reflect.Ptr { kind = fieldType.Type.Elem().Kind() @@ -371,47 +309,11 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) kind = fieldType.Type.Kind() } - var numericValue reflect.Value + numericValue, err := handleNumeric(value, kind) - switch kind { - case reflect.Int: - n := int(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Int8: - n := int8(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Int16: - n := int16(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Int32: - n := int32(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Int64: - n := int64(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Uint: - n := uint(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Uint8: - n := uint8(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Uint16: - n := uint16(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Uint32: - n := uint32(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Uint64: - n := uint64(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Float32: - n := float32(floatValue) - numericValue = reflect.ValueOf(&n) - case reflect.Float64: - n := floatValue - numericValue = reflect.ValueOf(&n) - default: - return ErrUnknownFieldNumberType + if err != nil { + er = err + break } assign(fieldValue, numericValue) @@ -420,36 +322,23 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) // Field was a Pointer type if fieldValue.Kind() == reflect.Ptr { - var concreteVal reflect.Value - switch cVal := val.(type) { - case string: - concreteVal = reflect.ValueOf(&cVal) - case bool: - concreteVal = reflect.ValueOf(&cVal) - case complex64: - concreteVal = reflect.ValueOf(&cVal) - case complex128: - concreteVal = reflect.ValueOf(&cVal) - case uintptr: - concreteVal = reflect.ValueOf(&cVal) - default: - return ErrUnsupportedPtrType + concreteVal, err := handlePointer(attribute, fieldValue.Type()) + + if err != nil { + er = err + break } - if fieldValue.Type() != concreteVal.Type() { - return ErrUnsupportedPtrType - } - - fieldValue.Set(concreteVal) + assign(fieldValue, concreteVal) continue } // As a final catch-all, ensure types line up to avoid a runtime panic. - if fieldValue.Kind() != v.Kind() { + if fieldValue.Kind() != value.Kind() { return ErrInvalidType } - fieldValue.Set(reflect.ValueOf(val)) + assign(fieldValue, reflect.ValueOf(attribute)) } else if annotation == annotationRelation { isSlice := fieldValue.Type().Kind() == reflect.Slice @@ -548,3 +437,119 @@ func assign(field, value reflect.Value) { field.Set(reflect.Indirect(value)) } } + +func handleStringSlice(v reflect.Value) []string { + values := make([]string, v.Len()) + for i := 0; i < v.Len(); i++ { + values[i] = v.Index(i).Interface().(string) + } + + return values +} + +func handleTime(v reflect.Value, isIso8601 bool) (time.Time, error) { + if isIso8601 { + var tm string + if v.Kind() == reflect.String { + tm = v.Interface().(string) + } else { + return time.Now(), ErrInvalidISO8601 + } + + t, err := time.Parse(iso8601TimeFormat, tm) + if err != nil { + return time.Now(), ErrInvalidISO8601 + } + + return t, nil + } + + var at int64 + + if v.Kind() == reflect.Float64 { + at = int64(v.Interface().(float64)) + } else if v.Kind() == reflect.Int { + at = v.Int() + } else { + return time.Now(), ErrInvalidTime + } + + t := time.Unix(at, 0) + + return t, nil +} + +func handleNumeric(v reflect.Value, kind reflect.Kind) (reflect.Value, error) { + + floatValue := v.Interface().(float64) + + var numericValue reflect.Value + + switch kind { + case reflect.Int: + n := int(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Int8: + n := int8(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Int16: + n := int16(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Int32: + n := int32(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Int64: + n := int64(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Uint: + n := uint(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Uint8: + n := uint8(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Uint16: + n := uint16(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Uint32: + n := uint32(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Uint64: + n := uint64(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Float32: + n := float32(floatValue) + numericValue = reflect.ValueOf(&n) + case reflect.Float64: + n := floatValue + numericValue = reflect.ValueOf(&n) + default: + return reflect.Value{}, ErrUnknownFieldNumberType + } + + return numericValue, nil +} + +func handlePointer(val interface{}, t reflect.Type) (reflect.Value, error) { + var concreteVal reflect.Value + + switch cVal := val.(type) { + case string: + concreteVal = reflect.ValueOf(&cVal) + case bool: + concreteVal = reflect.ValueOf(&cVal) + case complex64: + concreteVal = reflect.ValueOf(&cVal) + case complex128: + concreteVal = reflect.ValueOf(&cVal) + case uintptr: + concreteVal = reflect.ValueOf(&cVal) + default: + return reflect.Value{}, ErrUnsupportedPtrType + } + + if t != concreteVal.Type() { + return reflect.Value{}, ErrUnsupportedPtrType + } + + return concreteVal, nil +} From ab915ccbc17b0e2ebc9763996ba0bec581b7374f Mon Sep 17 00:00:00 2001 From: Slemgrim Date: Mon, 10 Jul 2017 22:50:43 +0200 Subject: [PATCH 02/17] unify type handler --- request.go | 108 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 62 insertions(+), 46 deletions(-) diff --git a/request.go b/request.go index 1090753..075e97c 100644 --- a/request.go +++ b/request.go @@ -117,6 +117,13 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) { return models, nil } +type unmarshal struct { + attribute interface{} + args []string + fieldType reflect.StructField + fieldValue reflect.Value +} + func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) { defer func() { @@ -245,20 +252,11 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) fieldValue.Set(reflect.ValueOf(data.ClientID)) } else if annotation == annotationAttribute { attributes := data.Attributes + if attributes == nil || len(data.Attributes) == 0 { continue } - var isIso8601 bool - - if len(args) > 2 { - for _, arg := range args[2:] { - if arg == annotationISO8601 { - isIso8601 = true - } - } - } - attribute := attributes[args[1]] // continue if the attribute was not included in the request @@ -266,50 +264,44 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) continue } + data := unmarshal{ + attribute, + args, + fieldType, + fieldValue, + } + + _ = data + value := reflect.ValueOf(attribute) // Handle field of type []string if fieldValue.Type() == reflect.TypeOf([]string{}) { - values := handleStringSlice(value) - assign(fieldValue, reflect.ValueOf(values)) + values, err := handleStringSlice(data) + if err != nil { + er = err + break + } + assign(fieldValue, values) continue } // Handle field of type time.Time - if fieldValue.Type() == reflect.TypeOf(time.Time{}) { - var time time.Time - if time, err = handleTime(value, isIso8601); err != nil { + if fieldValue.Type() == reflect.TypeOf(time.Time{}) || fieldValue.Type() == reflect.TypeOf(new(time.Time)) { + var time reflect.Value + if time, err = handleTime(data); err != nil { er = err break } - assign(fieldValue, reflect.ValueOf(time)) - continue - } - - // Handle field of type *time.Time - if fieldValue.Type() == reflect.TypeOf(new(time.Time)) { - var time time.Time - if time, err = handleTime(value, isIso8601); err != nil { - er = err - break - } - - assign(fieldValue, reflect.ValueOf(&time)) + assign(fieldValue, time) continue } // JSON value was a float (numeric) if value.Kind() == reflect.Float64 { - var kind reflect.Kind - if fieldValue.Kind() == reflect.Ptr { - kind = fieldType.Type.Elem().Kind() - } else { - kind = fieldType.Type.Kind() - } - - numericValue, err := handleNumeric(value, kind) + numericValue, err := handleNumeric(data) if err != nil { er = err @@ -438,30 +430,47 @@ func assign(field, value reflect.Value) { } } -func handleStringSlice(v reflect.Value) []string { +func handleStringSlice(m unmarshal) (reflect.Value, error) { + v := reflect.ValueOf(m.attribute) values := make([]string, v.Len()) for i := 0; i < v.Len(); i++ { values[i] = v.Index(i).Interface().(string) } - return values + return reflect.ValueOf(values), nil } -func handleTime(v reflect.Value, isIso8601 bool) (time.Time, error) { +func handleTime(m unmarshal) (reflect.Value, error) { + + var isIso8601 bool + v := reflect.ValueOf(m.attribute) + + if len(m.args) > 2 { + for _, arg := range m.args[2:] { + if arg == annotationISO8601 { + isIso8601 = true + } + } + } + if isIso8601 { var tm string if v.Kind() == reflect.String { tm = v.Interface().(string) } else { - return time.Now(), ErrInvalidISO8601 + return reflect.ValueOf(time.Now()), ErrInvalidISO8601 } t, err := time.Parse(iso8601TimeFormat, tm) if err != nil { - return time.Now(), ErrInvalidISO8601 + return reflect.ValueOf(time.Now()), ErrInvalidISO8601 } - return t, nil + if m.fieldValue.Kind() == reflect.Ptr { + return reflect.ValueOf(&t), nil + } + + return reflect.ValueOf(t), nil } var at int64 @@ -471,18 +480,25 @@ func handleTime(v reflect.Value, isIso8601 bool) (time.Time, error) { } else if v.Kind() == reflect.Int { at = v.Int() } else { - return time.Now(), ErrInvalidTime + return reflect.ValueOf(time.Now()), ErrInvalidTime } t := time.Unix(at, 0) - return t, nil + return reflect.ValueOf(t), nil } -func handleNumeric(v reflect.Value, kind reflect.Kind) (reflect.Value, error) { - +func handleNumeric(m unmarshal) (reflect.Value, error) { + v := reflect.ValueOf(m.attribute) floatValue := v.Interface().(float64) + var kind reflect.Kind + if m.fieldValue.Kind() == reflect.Ptr { + kind = m.fieldType.Type.Elem().Kind() + } else { + kind = m.fieldType.Type.Kind() + } + var numericValue reflect.Value switch kind { From 0c97f0cf8d95943002c92dcdf009e1f9ba5eac1e Mon Sep 17 00:00:00 2001 From: Slemgrim Date: Tue, 11 Jul 2017 21:43:12 +0200 Subject: [PATCH 03/17] extract attribute handling into custom method for recursive usage later on --- request.go | 130 ++++++++++++++++++++++++----------------------------- 1 file changed, 58 insertions(+), 72 deletions(-) diff --git a/request.go b/request.go index 075e97c..104eb78 100644 --- a/request.go +++ b/request.go @@ -271,66 +271,14 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) fieldValue, } - _ = data - - value := reflect.ValueOf(attribute) - - // Handle field of type []string - if fieldValue.Type() == reflect.TypeOf([]string{}) { - values, err := handleStringSlice(data) - if err != nil { - er = err - break - } - assign(fieldValue, values) - continue + value, err := unmarshalAttribute(data) + if err != nil { + er = err + break } - // Handle field of type time.Time - if fieldValue.Type() == reflect.TypeOf(time.Time{}) || fieldValue.Type() == reflect.TypeOf(new(time.Time)) { - var time reflect.Value - if time, err = handleTime(data); err != nil { - er = err - break - } - - assign(fieldValue, time) - continue - } - - // JSON value was a float (numeric) - if value.Kind() == reflect.Float64 { - - numericValue, err := handleNumeric(data) - - if err != nil { - er = err - break - } - - assign(fieldValue, numericValue) - continue - } - - // Field was a Pointer type - if fieldValue.Kind() == reflect.Ptr { - - concreteVal, err := handlePointer(attribute, fieldValue.Type()) - - if err != nil { - er = err - break - } - - assign(fieldValue, concreteVal) - continue - } - - // As a final catch-all, ensure types line up to avoid a runtime panic. - if fieldValue.Kind() != value.Kind() { - return ErrInvalidType - } - assign(fieldValue, reflect.ValueOf(attribute)) + assign(fieldValue, value) + continue } else if annotation == annotationRelation { isSlice := fieldValue.Type().Kind() == reflect.Slice @@ -430,8 +378,45 @@ func assign(field, value reflect.Value) { } } -func handleStringSlice(m unmarshal) (reflect.Value, error) { - v := reflect.ValueOf(m.attribute) +func unmarshalAttribute(data unmarshal) (value reflect.Value, err error) { + + value = reflect.ValueOf(data.attribute) + + // Handle field of type []string + if data.fieldValue.Type() == reflect.TypeOf([]string{}) { + value, err = handleStringSlice(data) + return + } + + // Handle field of type time.Time + if data.fieldValue.Type() == reflect.TypeOf(time.Time{}) || data.fieldValue.Type() == reflect.TypeOf(new(time.Time)) { + value, err = handleTime(data) + return + } + + // JSON value was a float (numeric) + if value.Kind() == reflect.Float64 { + value, err = handleNumeric(data) + return + } + + // Field was a Pointer type + if data.fieldValue.Kind() == reflect.Ptr { + value, err = handlePointer(data) + return + } + + // As a final catch-all, ensure types line up to avoid a runtime panic. + if data.fieldValue.Kind() != value.Kind() { + err = ErrInvalidType + return + } + + return +} + +func handleStringSlice(data unmarshal) (reflect.Value, error) { + v := reflect.ValueOf(data.attribute) values := make([]string, v.Len()) for i := 0; i < v.Len(); i++ { values[i] = v.Index(i).Interface().(string) @@ -440,13 +425,13 @@ func handleStringSlice(m unmarshal) (reflect.Value, error) { return reflect.ValueOf(values), nil } -func handleTime(m unmarshal) (reflect.Value, error) { +func handleTime(data unmarshal) (reflect.Value, error) { var isIso8601 bool - v := reflect.ValueOf(m.attribute) + v := reflect.ValueOf(data.attribute) - if len(m.args) > 2 { - for _, arg := range m.args[2:] { + if len(data.args) > 2 { + for _, arg := range data.args[2:] { if arg == annotationISO8601 { isIso8601 = true } @@ -466,7 +451,7 @@ func handleTime(m unmarshal) (reflect.Value, error) { return reflect.ValueOf(time.Now()), ErrInvalidISO8601 } - if m.fieldValue.Kind() == reflect.Ptr { + if data.fieldValue.Kind() == reflect.Ptr { return reflect.ValueOf(&t), nil } @@ -488,15 +473,15 @@ func handleTime(m unmarshal) (reflect.Value, error) { return reflect.ValueOf(t), nil } -func handleNumeric(m unmarshal) (reflect.Value, error) { - v := reflect.ValueOf(m.attribute) +func handleNumeric(data unmarshal) (reflect.Value, error) { + v := reflect.ValueOf(data.attribute) floatValue := v.Interface().(float64) var kind reflect.Kind - if m.fieldValue.Kind() == reflect.Ptr { - kind = m.fieldType.Type.Elem().Kind() + if data.fieldValue.Kind() == reflect.Ptr { + kind = data.fieldType.Type.Elem().Kind() } else { - kind = m.fieldType.Type.Kind() + kind = data.fieldType.Type.Kind() } var numericValue reflect.Value @@ -545,10 +530,11 @@ func handleNumeric(m unmarshal) (reflect.Value, error) { return numericValue, nil } -func handlePointer(val interface{}, t reflect.Type) (reflect.Value, error) { +func handlePointer(data unmarshal) (reflect.Value, error) { + t := data.fieldValue.Type() var concreteVal reflect.Value - switch cVal := val.(type) { + switch cVal := data.attribute.(type) { case string: concreteVal = reflect.ValueOf(&cVal) case bool: From af21dba1b2b491dc343377b9d7f04b1244892dd6 Mon Sep 17 00:00:00 2001 From: Slemgrim Date: Sat, 15 Jul 2017 15:22:03 +0200 Subject: [PATCH 04/17] handle nested struct slices --- models_test.go | 21 +++++++ request.go | 150 +++++++++++++++++++++++++++++++++++------------- request_test.go | 106 ++++++++++++++++++++++++++++++++++ 3 files changed, 236 insertions(+), 41 deletions(-) diff --git a/models_test.go b/models_test.go index a53dd61..d443378 100644 --- a/models_test.go +++ b/models_test.go @@ -155,3 +155,24 @@ func (bc *BadComment) JSONAPILinks() *Links { "self": []string{"invalid", "should error"}, } } + +type Company struct { + ID string `jsonapi:"primary,companies"` + Name string `jsonapi:"attr,name"` + Boss Employee `jsonapi:"attr,boss"` + Teams []Team `jsonapi:"attr,teams"` + FoundedAt time.Time `jsonapi:"attr,founded-at,iso8601"` +} + +type Team struct { + Name string `jsonapi:"attr,name"` + Leader *Employee `jsonapi:"attr,leader"` + Members []Employee `jsonapi:"attr,members"` +} + +type Employee struct { + Firstname string `jsonapi:"attr,firstname"` + Surname string `jsonapi:"attr,surname"` + Age int `jsonapi:"attr,age"` + HiredAt *time.Time `jsonapi:"attr,hired-at,iso8601"` +} diff --git a/request.go b/request.go index 104eb78..46863e9 100644 --- a/request.go +++ b/request.go @@ -117,13 +117,6 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) { return models, nil } -type unmarshal struct { - attribute interface{} - args []string - fieldType reflect.StructField - fieldValue reflect.Value -} - func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) { defer func() { @@ -147,7 +140,6 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) fieldValue := modelValue.Field(i) args := strings.Split(tag, ",") - if len(args) < 1 { er = ErrBadJSONAPIStructTag break @@ -264,14 +256,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) continue } - data := unmarshal{ - attribute, - args, - fieldType, - fieldValue, - } - - value, err := unmarshalAttribute(data) + value, err := unmarshalAttribute(attribute, args, fieldType.Type, fieldValue) if err != nil { er = err break @@ -378,36 +363,54 @@ func assign(field, value reflect.Value) { } } -func unmarshalAttribute(data unmarshal) (value reflect.Value, err error) { +func unmarshalAttribute(attribute interface{}, args []string, fieldType reflect.Type, fieldValue reflect.Value) (value reflect.Value, err error) { - value = reflect.ValueOf(data.attribute) + value = reflect.ValueOf(attribute) // Handle field of type []string - if data.fieldValue.Type() == reflect.TypeOf([]string{}) { - value, err = handleStringSlice(data) + if fieldValue.Type() == reflect.TypeOf([]string{}) { + value, err = handleStringSlice(attribute, args, fieldType, fieldValue) return } // Handle field of type time.Time - if data.fieldValue.Type() == reflect.TypeOf(time.Time{}) || data.fieldValue.Type() == reflect.TypeOf(new(time.Time)) { - value, err = handleTime(data) + if fieldValue.Type() == reflect.TypeOf(time.Time{}) || fieldValue.Type() == reflect.TypeOf(new(time.Time)) { + value, err = handleTime(attribute, args, fieldType, fieldValue) + return + } + + // Handle field of type struct + if fieldValue.Type().Kind() == reflect.Struct { + value, err = handleStruct(attribute, args, fieldType, fieldValue) + return + } + + // Handle field of type struct + if fieldValue.Type().Kind() == reflect.Struct { + value, err = handleStruct(attribute, args, fieldType, fieldValue) + return + } + + // Handle field containing slice of structs + if fieldValue.Type().Kind() == reflect.Slice && reflect.TypeOf(fieldValue.Interface()).Elem().Kind() == reflect.Struct { + value, err = handleStructSlice(attribute, args, fieldType, fieldValue) return } // JSON value was a float (numeric) if value.Kind() == reflect.Float64 { - value, err = handleNumeric(data) + value, err = handleNumeric(attribute, args, fieldType, fieldValue) return } // Field was a Pointer type - if data.fieldValue.Kind() == reflect.Ptr { - value, err = handlePointer(data) + if fieldValue.Kind() == reflect.Ptr { + value, err = handlePointer(attribute, args, fieldType, fieldValue) return } // As a final catch-all, ensure types line up to avoid a runtime panic. - if data.fieldValue.Kind() != value.Kind() { + if fieldValue.Kind() != value.Kind() { err = ErrInvalidType return } @@ -415,8 +418,8 @@ func unmarshalAttribute(data unmarshal) (value reflect.Value, err error) { return } -func handleStringSlice(data unmarshal) (reflect.Value, error) { - v := reflect.ValueOf(data.attribute) +func handleStringSlice(attribute interface{}, args []string, fieldType reflect.Type, fieldValue reflect.Value) (reflect.Value, error) { + v := reflect.ValueOf(attribute) values := make([]string, v.Len()) for i := 0; i < v.Len(); i++ { values[i] = v.Index(i).Interface().(string) @@ -425,13 +428,13 @@ func handleStringSlice(data unmarshal) (reflect.Value, error) { return reflect.ValueOf(values), nil } -func handleTime(data unmarshal) (reflect.Value, error) { +func handleTime(attribute interface{}, args []string, fieldType reflect.Type, fieldValue reflect.Value) (reflect.Value, error) { var isIso8601 bool - v := reflect.ValueOf(data.attribute) + v := reflect.ValueOf(attribute) - if len(data.args) > 2 { - for _, arg := range data.args[2:] { + if len(args) > 2 { + for _, arg := range args[2:] { if arg == annotationISO8601 { isIso8601 = true } @@ -451,7 +454,7 @@ func handleTime(data unmarshal) (reflect.Value, error) { return reflect.ValueOf(time.Now()), ErrInvalidISO8601 } - if data.fieldValue.Kind() == reflect.Ptr { + if fieldValue.Kind() == reflect.Ptr { return reflect.ValueOf(&t), nil } @@ -473,15 +476,15 @@ func handleTime(data unmarshal) (reflect.Value, error) { return reflect.ValueOf(t), nil } -func handleNumeric(data unmarshal) (reflect.Value, error) { - v := reflect.ValueOf(data.attribute) +func handleNumeric(attribute interface{}, args []string, fieldType reflect.Type, fieldValue reflect.Value) (reflect.Value, error) { + v := reflect.ValueOf(attribute) floatValue := v.Interface().(float64) var kind reflect.Kind - if data.fieldValue.Kind() == reflect.Ptr { - kind = data.fieldType.Type.Elem().Kind() + if fieldValue.Kind() == reflect.Ptr { + kind = fieldType.Elem().Kind() } else { - kind = data.fieldType.Type.Kind() + kind = fieldType.Kind() } var numericValue reflect.Value @@ -530,11 +533,11 @@ func handleNumeric(data unmarshal) (reflect.Value, error) { return numericValue, nil } -func handlePointer(data unmarshal) (reflect.Value, error) { - t := data.fieldValue.Type() +func handlePointer(attribute interface{}, args []string, fieldType reflect.Type, fieldValue reflect.Value) (reflect.Value, error) { + t := fieldValue.Type() var concreteVal reflect.Value - switch cVal := data.attribute.(type) { + switch cVal := attribute.(type) { case string: concreteVal = reflect.ValueOf(&cVal) case bool: @@ -555,3 +558,68 @@ func handlePointer(data unmarshal) (reflect.Value, error) { return concreteVal, nil } + +func handleStruct(attribute interface{}, args []string, fieldType reflect.Type, fieldValue reflect.Value) (reflect.Value, error) { + model := reflect.New(fieldValue.Type()) + + modelValue := model.Elem() + modelType := model.Type().Elem() + + var er error + + for i := 0; i < modelValue.NumField(); i++ { + fieldType := modelType.Field(i) + tag := fieldType.Tag.Get("jsonapi") + if tag == "" { + continue + } + + fieldValue := modelValue.Field(i) + + args := strings.Split(tag, ",") + + if len(args) < 1 { + er = ErrBadJSONAPIStructTag + break + } + + if reflect.TypeOf(attribute).Kind() != reflect.Map { + return model, nil + } + + attributes := reflect.ValueOf(attribute).Interface().(map[string]interface{}) + attribute := attributes[args[1]] + + if attribute == nil { + continue + } + + value, err := unmarshalAttribute(attribute, args, fieldType.Type, fieldValue) + if err != nil { + return model, nil + } + + assign(fieldValue, value) + } + + return model, er +} + +func handleStructSlice(attribute interface{}, args []string, fieldType reflect.Type, fieldValue reflect.Value) (reflect.Value, error) { + models := reflect.New(fieldValue.Type()).Elem() + dataMap := reflect.ValueOf(attribute).Interface().([]interface{}) + for _, data := range dataMap { + model := reflect.New(fieldValue.Type().Elem()).Elem() + modelType := model.Type() + + value, err := handleStruct(data, []string{}, modelType, model) + + if err != nil { + continue + } + + models = reflect.Append(models, reflect.Indirect(value)) + } + + return models, nil +} diff --git a/request_test.go b/request_test.go index 6b47fd7..616adbf 100644 --- a/request_test.go +++ b/request_test.go @@ -945,3 +945,109 @@ func sampleSerializedEmbeddedTestModel() *Blog { return blog } + +func TestUnmarshalNestedStruct(t *testing.T) { + + boss := map[string]interface{}{ + "firstname": "Hubert", + "surname": "Farnsworth", + "age": 176, + "hired-at": "2016-08-17T08:27:12Z", + } + + sample := map[string]interface{}{ + "data": map[string]interface{}{ + "type": "companies", + "id": "123", + "attributes": map[string]interface{}{ + "name": "Planet Express", + "boss": boss, + "founded-at": "2016-08-17T08:27:12Z", + }, + }, + } + + data, err := json.Marshal(sample) + if err != nil { + t.Fatal(err) + } + in := bytes.NewReader(data) + out := new(Company) + + if err := UnmarshalPayload(in, out); err != nil { + t.Fatal(err) + } + + if out.Boss.Firstname != "Hubert" { + t.Fatalf("Nested struct was not unmarshalled") + } + + if out.Boss.Age != 176 { + t.Fatalf("Nested struct was not unmarshalled") + } + + if out.Boss.HiredAt.IsZero() { + t.Fatalf("Nested struct was not unmarshalled") + } +} + +func TestUnmarshalNestedStructSlice(t *testing.T) { + + fry := map[string]interface{}{ + "firstname": "Philip J.", + "surname": "Fry", + "age": 25, + "hired-at": "2016-08-17T08:27:12Z", + } + + bender := map[string]interface{}{ + "firstname": "Bender Bending", + "surname": "Rodriguez", + "age": 19, + "hired-at": "2016-08-17T08:27:12Z", + } + + deliveryCrew := map[string]interface{}{ + "name": "Delivery Crew", + "members": []interface{}{ + fry, + bender, + }, + } + + sample := map[string]interface{}{ + "data": map[string]interface{}{ + "type": "companies", + "id": "123", + "attributes": map[string]interface{}{ + "name": "Planet Express", + "teams": []interface{}{ + deliveryCrew, + }, + }, + }, + } + + data, err := json.Marshal(sample) + if err != nil { + t.Fatal(err) + } + in := bytes.NewReader(data) + out := new(Company) + + if err := UnmarshalPayload(in, out); err != nil { + t.Fatal(err) + } + + if out.Teams[0].Name != "Delivery Crew" { + t.Fatalf("Nested struct Team was not unmarshalled") + } + + if len(out.Teams[0].Members) != 2 { + t.Fatalf("Nested struct Members were not unmarshalled") + } + + if out.Teams[0].Members[0].Firstname != "Philip J." { + t.Fatalf("Nested struct member was not unmarshalled") + } +} From b391a84b7581b70a4ce54ecf5b1a1fcb1bcc79e3 Mon Sep 17 00:00:00 2001 From: Markus Ritberger Date: Mon, 14 Aug 2017 20:39:14 +0200 Subject: [PATCH 05/17] remove duplicated code --- request.go | 47 +++++++---------------------------------------- 1 file changed, 7 insertions(+), 40 deletions(-) diff --git a/request.go b/request.go index 46863e9..88bf2ba 100644 --- a/request.go +++ b/request.go @@ -385,12 +385,6 @@ func unmarshalAttribute(attribute interface{}, args []string, fieldType reflect. return } - // Handle field of type struct - if fieldValue.Type().Kind() == reflect.Struct { - value, err = handleStruct(attribute, args, fieldType, fieldValue) - return - } - // Handle field containing slice of structs if fieldValue.Type().Kind() == reflect.Slice && reflect.TypeOf(fieldValue.Interface()).Elem().Kind() == reflect.Struct { value, err = handleStructSlice(attribute, args, fieldType, fieldValue) @@ -562,44 +556,17 @@ func handlePointer(attribute interface{}, args []string, fieldType reflect.Type, func handleStruct(attribute interface{}, args []string, fieldType reflect.Type, fieldValue reflect.Value) (reflect.Value, error) { model := reflect.New(fieldValue.Type()) - modelValue := model.Elem() - modelType := model.Type().Elem() - var er error - for i := 0; i < modelValue.NumField(); i++ { - fieldType := modelType.Field(i) - tag := fieldType.Tag.Get("jsonapi") - if tag == "" { - continue - } + data, er := json.Marshal(attribute) + if er != nil { + return model, er + } - fieldValue := modelValue.Field(i) + er = json.Unmarshal(data, model.Interface()) - args := strings.Split(tag, ",") - - if len(args) < 1 { - er = ErrBadJSONAPIStructTag - break - } - - if reflect.TypeOf(attribute).Kind() != reflect.Map { - return model, nil - } - - attributes := reflect.ValueOf(attribute).Interface().(map[string]interface{}) - attribute := attributes[args[1]] - - if attribute == nil { - continue - } - - value, err := unmarshalAttribute(attribute, args, fieldType.Type, fieldValue) - if err != nil { - return model, nil - } - - assign(fieldValue, value) + if er != nil { + return model, er } return model, er From fc6968dfe7f254d8285e5ba958f5f6d5514c7436 Mon Sep 17 00:00:00 2001 From: Markus Ritberger Date: Mon, 14 Aug 2017 20:41:26 +0200 Subject: [PATCH 06/17] change nested structs to json annotation instead of json:api. It never made sense to use json:api annotation in nested structs --- models_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/models_test.go b/models_test.go index d443378..eb38c98 100644 --- a/models_test.go +++ b/models_test.go @@ -165,14 +165,14 @@ type Company struct { } type Team struct { - Name string `jsonapi:"attr,name"` - Leader *Employee `jsonapi:"attr,leader"` - Members []Employee `jsonapi:"attr,members"` + Name string `json:"name"` + Leader *Employee `json:"leader"` + Members []Employee `json:"members"` } type Employee struct { - Firstname string `jsonapi:"attr,firstname"` - Surname string `jsonapi:"attr,surname"` - Age int `jsonapi:"attr,age"` - HiredAt *time.Time `jsonapi:"attr,hired-at,iso8601"` + Firstname string `json:"firstname"` + Surname string `json:"surname"` + Age int `json:"age"` + HiredAt *time.Time `json:"hired-at,iso8601"` } From a6577dfae8d8950d26c58b9d6cb906133c41ce12 Mon Sep 17 00:00:00 2001 From: Slemgrim Date: Tue, 21 Nov 2017 19:05:50 +0800 Subject: [PATCH 07/17] Add more speaking tests tests should have an expected outcome vs. an actual outcome to make them easier to debug --- request_test.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/request_test.go b/request_test.go index 21f0fcd..8c2e519 100644 --- a/request_test.go +++ b/request_test.go @@ -979,15 +979,15 @@ func TestUnmarshalNestedStruct(t *testing.T) { } if out.Boss.Firstname != "Hubert" { - t.Fatalf("Nested struct was not unmarshalled") + t.Fatalf("expected `Hubert` at out.Boss.Firstname, but got `%s`", out.Boss.Firstname) } if out.Boss.Age != 176 { - t.Fatalf("Nested struct was not unmarshalled") + t.Fatalf("expected `176` at out.Boss.Age, but got `%d`", out.Boss.Age) } if out.Boss.HiredAt.IsZero() { - t.Fatalf("Nested struct was not unmarshalled") + t.Fatalf("expected out.Boss.HiredAt to be zero, but got `%d`", out.Boss.HiredAt) } } @@ -1040,14 +1040,16 @@ func TestUnmarshalNestedStructSlice(t *testing.T) { } if out.Teams[0].Name != "Delivery Crew" { - t.Fatalf("Nested struct Team was not unmarshalled") + t.Fatalf("Nested struct not unmarshalled: Expected `Delivery Crew` but got `%s`", out.Teams[0].Name) } if len(out.Teams[0].Members) != 2 { - t.Fatalf("Nested struct Members were not unmarshalled") + t.Fatalf("Nested struct not unmarshalled: Expected to have `2` Members but got `%d`", + len(out.Teams[0].Members)) } if out.Teams[0].Members[0].Firstname != "Philip J." { - t.Fatalf("Nested struct member was not unmarshalled") + t.Fatalf("Nested struct not unmarshalled: Expected `Philip J.` but got `%s`", + out.Teams[0].Members[0].Firstname) } } From 04000417716554a8abc793978e4acac61f179ec0 Mon Sep 17 00:00:00 2001 From: Slemgrim Date: Tue, 21 Nov 2017 19:15:24 +0800 Subject: [PATCH 08/17] fix wrong type for formatting --- request_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/request_test.go b/request_test.go index 8c2e519..7f58827 100644 --- a/request_test.go +++ b/request_test.go @@ -987,7 +987,7 @@ func TestUnmarshalNestedStruct(t *testing.T) { } if out.Boss.HiredAt.IsZero() { - t.Fatalf("expected out.Boss.HiredAt to be zero, but got `%d`", out.Boss.HiredAt) + t.Fatalf("expected out.Boss.HiredAt to be zero, but got `%t`", out.Boss.HiredAt.IsZero()) } } From e3c0871f344086fb80cf3d95bbb96c011ef4031c Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Wed, 17 Jan 2018 08:03:13 -0800 Subject: [PATCH 09/17] Test more things in TestUnmarshalNestedStruct --- request_test.go | 62 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/request_test.go b/request_test.go index 7f58827..b0054aa 100644 --- a/request_test.go +++ b/request_test.go @@ -963,6 +963,24 @@ func TestUnmarshalNestedStruct(t *testing.T) { "name": "Planet Express", "boss": boss, "founded-at": "2016-08-17T08:27:12Z", + "teams": []Team{ + Team{ + Name: "Dev", + Members: []Employee{ + Employee{Firstname: "Sean"}, + Employee{Firstname: "Iz"}, + }, + Leader: &Employee{Firstname: "Iz"}, + }, + Team{ + Name: "DxE", + Members: []Employee{ + Employee{Firstname: "Akshay"}, + Employee{Firstname: "Peri"}, + }, + Leader: &Employee{Firstname: "Peri"}, + }, + }, }, }, } @@ -989,6 +1007,50 @@ func TestUnmarshalNestedStruct(t *testing.T) { if out.Boss.HiredAt.IsZero() { t.Fatalf("expected out.Boss.HiredAt to be zero, but got `%t`", out.Boss.HiredAt.IsZero()) } + + if len(out.Teams) != 2 { + t.Fatalf("expected len(out.Teams) to be 2, but got `%d`", len(out.Teams)) + } + + if out.Teams[0].Name != "Dev" { + t.Fatalf("expected out.Teams[0].Name to be `Dev`, but got `%s`", out.Teams[0].Name) + } + + if out.Teams[1].Name != "DxE" { + t.Fatalf("expected out.Teams[1].Name to be `DxE`, but got `%s`", out.Teams[1].Name) + } + + if len(out.Teams[0].Members) != 2 { + t.Fatalf("expected len(out.Teams[0].Members) to be 2, but got `%d`", len(out.Teams[0].Members)) + } + + if len(out.Teams[1].Members) != 2 { + t.Fatalf("expected len(out.Teams[1].Members) to be 2, but got `%d`", len(out.Teams[1].Members)) + } + + if out.Teams[0].Members[0].Firstname != "Sean" { + t.Fatalf("expected out.Teams[0].Members[0].Firstname to be `Sean`, but got `%s`", out.Teams[0].Members[0].Firstname) + } + + if out.Teams[0].Members[1].Firstname != "Iz" { + t.Fatalf("expected out.Teams[0].Members[1].Firstname to be `Iz`, but got `%s`", out.Teams[0].Members[1].Firstname) + } + + if out.Teams[1].Members[0].Firstname != "Akshay" { + t.Fatalf("expected out.Teams[1].Members[0].Firstname to be `Akshay`, but got `%s`", out.Teams[1].Members[0].Firstname) + } + + if out.Teams[1].Members[1].Firstname != "Peri" { + t.Fatalf("expected out.Teams[1].Members[1].Firstname to be `Peri`, but got `%s`", out.Teams[1].Members[1].Firstname) + } + + if out.Teams[0].Leader.Firstname != "Iz" { + t.Fatalf("expected out.Teams[0].Leader.Firstname to be `Iz`, but got `%s`", out.Teams[0].Leader.Firstname) + } + + if out.Teams[1].Leader.Firstname != "Peri" { + t.Fatalf("expected out.Teams[1].Leader.Firstname to be `Peri`, but got `%s`", out.Teams[1].Leader.Firstname) + } } func TestUnmarshalNestedStructSlice(t *testing.T) { From b28beab7f35199f3b4401d5797c158eae935f1cf Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Wed, 17 Jan 2018 13:02:09 -0800 Subject: [PATCH 10/17] Add TestUnmarshalNestedStructPtr --- request_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/request_test.go b/request_test.go index 7f58827..2016143 100644 --- a/request_test.go +++ b/request_test.go @@ -946,6 +946,52 @@ func sampleSerializedEmbeddedTestModel() *Blog { return blog } +func TestUnmarshalNestedStructPtr(t *testing.T) { + type Director struct { + Firstname string `json:"firstname"` + Surname string `json:"surname"` + } + type Movie struct { + ID string `jsonapi:"primary,movies"` + Name string `jsonapi:"attr,name"` + Director *Director `jsonapi:"attr,director"` + } + sample := map[string]interface{}{ + "data": map[string]interface{}{ + "type": "movies", + "id": "123", + "attributes": map[string]interface{}{ + "name": "The Shawshank Redemption", + "director": map[string]interface{}{ + "firstname": "Frank", + "surname": "Darabont", + }, + }, + }, + } + + data, err := json.Marshal(sample) + if err != nil { + t.Fatal(err) + } + in := bytes.NewReader(data) + out := new(Movie) + + if err := UnmarshalPayload(in, out); err != nil { + t.Fatal(err) + } + + if out.Name != "The Shawshank Redemption" { + t.Fatalf("expected out.Name to be `The Shawshank Redemption`, but got `%s`", out.Name) + } + if out.Director.Firstname != "Frank" { + t.Fatalf("expected out.Director.Firstname to be `Frank`, but got `%s`", out.Director.Firstname) + } + if out.Director.Surname != "Darabont" { + t.Fatalf("expected out.Director.Surname to be `Darabont`, but got `%s`", out.Director.Surname) + } +} + func TestUnmarshalNestedStruct(t *testing.T) { boss := map[string]interface{}{ From 16e19ab9f9b35a130eab32ed7a02b567709d737d Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Wed, 17 Jan 2018 11:40:21 -0800 Subject: [PATCH 11/17] Make nested struct pointers work --- request.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/request.go b/request.go index 88bf2ba..da42c05 100644 --- a/request.go +++ b/request.go @@ -542,6 +542,10 @@ func handlePointer(attribute interface{}, args []string, fieldType reflect.Type, concreteVal = reflect.ValueOf(&cVal) case uintptr: concreteVal = reflect.ValueOf(&cVal) + case map[string]interface{}: + var err error + concreteVal, err = handleStruct(attribute, args, fieldType, fieldValue) + return concreteVal.Elem(), err default: return reflect.Value{}, ErrUnsupportedPtrType } From a3b3bb2cb50b12fa45da164f76316a9d09c3157c Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Wed, 17 Jan 2018 10:11:20 -0800 Subject: [PATCH 12/17] Show type for ErrUnsupportedPtrType to aid in troubleshooting. Before: ``` Pointer type in struct is not supported ``` After: ``` jsonapi: Can't unmarshal true (bool) to struct field `Name`, which is a pointer to `string` ``` --- request.go | 31 +++++++++++++++++++-------- request_test.go | 57 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 11 deletions(-) diff --git a/request.go b/request.go index 88bf2ba..44a946f 100644 --- a/request.go +++ b/request.go @@ -27,13 +27,24 @@ var ( // (numeric) but the Struct field was a non numeric type (i.e. not int, uint, // float, etc) ErrUnknownFieldNumberType = errors.New("The struct field was not of a known number type") - // ErrUnsupportedPtrType is returned when the Struct field was a pointer but - // the JSON value was of a different type - ErrUnsupportedPtrType = errors.New("Pointer type in struct is not supported") // ErrInvalidType is returned when the given type is incompatible with the expected type. ErrInvalidType = errors.New("Invalid type provided") // I wish we used punctuation. ) +// ErrUnsupportedPtrType is returned when the Struct field was a pointer but +// the JSON value was of a different type +func ErrUnsupportedPtrType(rf reflect.Value, t reflect.Type, structField reflect.StructField) error { + typeName := t.Elem().Name() + kind := t.Elem().Kind() + if kind.String() != "" && kind.String() != typeName { + typeName = fmt.Sprintf("%s (%s)", typeName, kind.String()) + } + return fmt.Errorf( + "jsonapi: Can't unmarshal %+v (%s) to struct field `%s`, which is a pointer to `%s`", + rf, rf.Type().Kind(), structField.Name, typeName, + ) +} + // UnmarshalPayload converts an io into a struct instance using jsonapi tags on // struct fields. This method supports single request payloads only, at the // moment. Bulk creates and updates are not supported yet. @@ -256,7 +267,8 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) continue } - value, err := unmarshalAttribute(attribute, args, fieldType.Type, fieldValue) + structField := fieldType + value, err := unmarshalAttribute(attribute, args, structField, fieldValue) if err != nil { er = err break @@ -363,9 +375,10 @@ func assign(field, value reflect.Value) { } } -func unmarshalAttribute(attribute interface{}, args []string, fieldType reflect.Type, fieldValue reflect.Value) (value reflect.Value, err error) { +func unmarshalAttribute(attribute interface{}, args []string, structField reflect.StructField, fieldValue reflect.Value) (value reflect.Value, err error) { value = reflect.ValueOf(attribute) + fieldType := structField.Type // Handle field of type []string if fieldValue.Type() == reflect.TypeOf([]string{}) { @@ -399,7 +412,7 @@ func unmarshalAttribute(attribute interface{}, args []string, fieldType reflect. // Field was a Pointer type if fieldValue.Kind() == reflect.Ptr { - value, err = handlePointer(attribute, args, fieldType, fieldValue) + value, err = handlePointer(attribute, args, fieldType, fieldValue, structField) return } @@ -527,7 +540,7 @@ func handleNumeric(attribute interface{}, args []string, fieldType reflect.Type, return numericValue, nil } -func handlePointer(attribute interface{}, args []string, fieldType reflect.Type, fieldValue reflect.Value) (reflect.Value, error) { +func handlePointer(attribute interface{}, args []string, fieldType reflect.Type, fieldValue reflect.Value, structField reflect.StructField) (reflect.Value, error) { t := fieldValue.Type() var concreteVal reflect.Value @@ -543,11 +556,11 @@ func handlePointer(attribute interface{}, args []string, fieldType reflect.Type, case uintptr: concreteVal = reflect.ValueOf(&cVal) default: - return reflect.Value{}, ErrUnsupportedPtrType + return reflect.Value{}, ErrUnsupportedPtrType(reflect.ValueOf(attribute), fieldType, structField) } if t != concreteVal.Type() { - return reflect.Value{}, ErrUnsupportedPtrType + return reflect.Value{}, ErrUnsupportedPtrType(reflect.ValueOf(attribute), fieldType, structField) } return concreteVal, nil diff --git a/request_test.go b/request_test.go index 7f58827..217595f 100644 --- a/request_test.go +++ b/request_test.go @@ -121,12 +121,65 @@ func TestUnmarshalPayloadWithPointerAttr_AbsentVal(t *testing.T) { } } -func TestUnmarshalToStructWithPointerAttr_BadType(t *testing.T) { +func TestUnmarshalToStructWithPointerAttr_BadType_bool(t *testing.T) { out := new(WithPointer) in := map[string]interface{}{ "name": true, // This is the wrong type. } - expectedErrorMessage := ErrUnsupportedPtrType.Error() + expectedErrorMessage := "jsonapi: Can't unmarshal true (bool) to struct field `Name`, which is a pointer to `string`" + + err := UnmarshalPayload(sampleWithPointerPayload(in), out) + + if err == nil { + t.Fatalf("Expected error due to invalid type.") + } + if err.Error() != expectedErrorMessage { + t.Fatalf("Unexpected error message: %s", err.Error()) + } +} + +func TestUnmarshalToStructWithPointerAttr_BadType_MapPtr(t *testing.T) { + out := new(WithPointer) + in := map[string]interface{}{ + "name": &map[string]interface{}{"a": 5}, // This is the wrong type. + } + expectedErrorMessage := "jsonapi: Can't unmarshal map[a:5] (map) to struct field `Name`, which is a pointer to `string`" + + err := UnmarshalPayload(sampleWithPointerPayload(in), out) + + if err == nil { + t.Fatalf("Expected error due to invalid type.") + } + if err.Error() != expectedErrorMessage { + t.Fatalf("Unexpected error message: %s", err.Error()) + } +} + +func TestUnmarshalToStructWithPointerAttr_BadType_Struct(t *testing.T) { + out := new(WithPointer) + type FooStruct struct{ A int } + in := map[string]interface{}{ + "name": FooStruct{A: 5}, // This is the wrong type. + } + expectedErrorMessage := "jsonapi: Can't unmarshal map[A:5] (map) to struct field `Name`, which is a pointer to `string`" + + err := UnmarshalPayload(sampleWithPointerPayload(in), out) + + if err == nil { + t.Fatalf("Expected error due to invalid type.") + } + if err.Error() != expectedErrorMessage { + t.Fatalf("Unexpected error message: %s", err.Error()) + } +} + +func TestUnmarshalToStructWithPointerAttr_BadType_IntSlice(t *testing.T) { + out := new(WithPointer) + type FooStruct struct{ A, B int } + in := map[string]interface{}{ + "name": []int{4, 5}, // This is the wrong type. + } + expectedErrorMessage := "jsonapi: Can't unmarshal [4 5] (slice) to struct field `Name`, which is a pointer to `string`" err := UnmarshalPayload(sampleWithPointerPayload(in), out) From 7c2ceac7c505bb329f2c0a6ee72e4730fbe9f81e Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Thu, 25 Jan 2018 07:28:20 -0800 Subject: [PATCH 13/17] Fix test failures --- request.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/request.go b/request.go index cc986f1..6e14c12 100644 --- a/request.go +++ b/request.go @@ -558,6 +558,9 @@ func handlePointer(attribute interface{}, args []string, fieldType reflect.Type, case map[string]interface{}: var err error concreteVal, err = handleStruct(attribute, args, fieldType, fieldValue) + if err != nil { + return reflect.Value{}, ErrUnsupportedPtrType(reflect.ValueOf(attribute), fieldType, structField) + } return concreteVal.Elem(), err default: return reflect.Value{}, ErrUnsupportedPtrType(reflect.ValueOf(attribute), fieldType, structField) From 9bc94d8c700dc4dbd131e4f5311780d637447de0 Mon Sep 17 00:00:00 2001 From: Markus Ritberger Date: Wed, 14 Mar 2018 20:52:27 +0100 Subject: [PATCH 14/17] replace public function with custom error type --- request.go | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/request.go b/request.go index 6e14c12..2c66c69 100644 --- a/request.go +++ b/request.go @@ -29,22 +29,33 @@ var ( ErrUnknownFieldNumberType = errors.New("The struct field was not of a known number type") // ErrInvalidType is returned when the given type is incompatible with the expected type. ErrInvalidType = errors.New("Invalid type provided") // I wish we used punctuation. + ) // ErrUnsupportedPtrType is returned when the Struct field was a pointer but // the JSON value was of a different type -func ErrUnsupportedPtrType(rf reflect.Value, t reflect.Type, structField reflect.StructField) error { - typeName := t.Elem().Name() - kind := t.Elem().Kind() +type ErrUnsupportedPtrType struct { + rf reflect.Value + t reflect.Type + structField reflect.StructField +} + +func (eupt ErrUnsupportedPtrType) Error() string { + typeName := eupt.t.Elem().Name() + kind := eupt.t.Elem().Kind() if kind.String() != "" && kind.String() != typeName { typeName = fmt.Sprintf("%s (%s)", typeName, kind.String()) } - return fmt.Errorf( + return fmt.Sprintf( "jsonapi: Can't unmarshal %+v (%s) to struct field `%s`, which is a pointer to `%s`", - rf, rf.Type().Kind(), structField.Name, typeName, + eupt.rf, eupt.rf.Type().Kind(), eupt.structField.Name, typeName, ) } +func newErrUnsupportedPtrType(rf reflect.Value, t reflect.Type, structField reflect.StructField) error { + return ErrUnsupportedPtrType{rf, t, structField} +} + // UnmarshalPayload converts an io into a struct instance using jsonapi tags on // struct fields. This method supports single request payloads only, at the // moment. Bulk creates and updates are not supported yet. @@ -559,15 +570,15 @@ func handlePointer(attribute interface{}, args []string, fieldType reflect.Type, var err error concreteVal, err = handleStruct(attribute, args, fieldType, fieldValue) if err != nil { - return reflect.Value{}, ErrUnsupportedPtrType(reflect.ValueOf(attribute), fieldType, structField) + return reflect.Value{}, newErrUnsupportedPtrType(reflect.ValueOf(attribute), fieldType, structField) } return concreteVal.Elem(), err default: - return reflect.Value{}, ErrUnsupportedPtrType(reflect.ValueOf(attribute), fieldType, structField) + return reflect.Value{}, newErrUnsupportedPtrType(reflect.ValueOf(attribute), fieldType, structField) } if t != concreteVal.Type() { - return reflect.Value{}, ErrUnsupportedPtrType(reflect.ValueOf(attribute), fieldType, structField) + return reflect.Value{}, newErrUnsupportedPtrType(reflect.ValueOf(attribute), fieldType, structField) } return concreteVal, nil From 72f7bad5b3662d9687cb3e4e3b2e085ac6761c5a Mon Sep 17 00:00:00 2001 From: Markus Ritberger Date: Wed, 14 Mar 2018 21:43:24 +0100 Subject: [PATCH 15/17] check for ptr error type in tests --- request_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/request_test.go b/request_test.go index 79ec030..8ee4a5a 100644 --- a/request_test.go +++ b/request_test.go @@ -136,6 +136,9 @@ func TestUnmarshalToStructWithPointerAttr_BadType_bool(t *testing.T) { if err.Error() != expectedErrorMessage { t.Fatalf("Unexpected error message: %s", err.Error()) } + if _, ok := err.(ErrUnsupportedPtrType); !ok { + t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err)) + } } func TestUnmarshalToStructWithPointerAttr_BadType_MapPtr(t *testing.T) { @@ -153,6 +156,9 @@ func TestUnmarshalToStructWithPointerAttr_BadType_MapPtr(t *testing.T) { if err.Error() != expectedErrorMessage { t.Fatalf("Unexpected error message: %s", err.Error()) } + if _, ok := err.(ErrUnsupportedPtrType); !ok { + t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err)) + } } func TestUnmarshalToStructWithPointerAttr_BadType_Struct(t *testing.T) { @@ -171,6 +177,9 @@ func TestUnmarshalToStructWithPointerAttr_BadType_Struct(t *testing.T) { if err.Error() != expectedErrorMessage { t.Fatalf("Unexpected error message: %s", err.Error()) } + if _, ok := err.(ErrUnsupportedPtrType); !ok { + t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err)) + } } func TestUnmarshalToStructWithPointerAttr_BadType_IntSlice(t *testing.T) { @@ -189,6 +198,9 @@ func TestUnmarshalToStructWithPointerAttr_BadType_IntSlice(t *testing.T) { if err.Error() != expectedErrorMessage { t.Fatalf("Unexpected error message: %s", err.Error()) } + if _, ok := err.(ErrUnsupportedPtrType); !ok { + t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err)) + } } func TestStringPointerField(t *testing.T) { From d490a0f637974f2f7a94d10911b8f62837d68cbe Mon Sep 17 00:00:00 2001 From: Markus Ritberger Date: Wed, 14 Mar 2018 21:43:51 +0100 Subject: [PATCH 16/17] remove whitespaces and stick to 80chars --- request.go | 83 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 54 insertions(+), 29 deletions(-) diff --git a/request.go b/request.go index 2c66c69..ff426ce 100644 --- a/request.go +++ b/request.go @@ -140,7 +140,6 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) { } func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) { - defer func() { if r := recover(); r != nil { err = fmt.Errorf("data is not a jsonapi representation of '%v'", model.Type()) @@ -386,8 +385,11 @@ func assign(field, value reflect.Value) { } } -func unmarshalAttribute(attribute interface{}, args []string, structField reflect.StructField, fieldValue reflect.Value) (value reflect.Value, err error) { - +func unmarshalAttribute( + attribute interface{}, + args []string, + structField reflect.StructField, + fieldValue reflect.Value) (value reflect.Value, err error) { value = reflect.ValueOf(attribute) fieldType := structField.Type @@ -398,7 +400,8 @@ func unmarshalAttribute(attribute interface{}, args []string, structField reflec } // Handle field of type time.Time - if fieldValue.Type() == reflect.TypeOf(time.Time{}) || fieldValue.Type() == reflect.TypeOf(new(time.Time)) { + if fieldValue.Type() == reflect.TypeOf(time.Time{}) || + fieldValue.Type() == reflect.TypeOf(new(time.Time)) { value, err = handleTime(attribute, args, fieldType, fieldValue) return } @@ -410,7 +413,8 @@ func unmarshalAttribute(attribute interface{}, args []string, structField reflec } // Handle field containing slice of structs - if fieldValue.Type().Kind() == reflect.Slice && reflect.TypeOf(fieldValue.Interface()).Elem().Kind() == reflect.Struct { + if fieldValue.Type().Kind() == reflect.Slice && + reflect.TypeOf(fieldValue.Interface()).Elem().Kind() == reflect.Struct { value, err = handleStructSlice(attribute, args, fieldType, fieldValue) return } @@ -436,7 +440,11 @@ func unmarshalAttribute(attribute interface{}, args []string, structField reflec return } -func handleStringSlice(attribute interface{}, args []string, fieldType reflect.Type, fieldValue reflect.Value) (reflect.Value, error) { +func handleStringSlice( + attribute interface{}, + args []string, + fieldType reflect.Type, + fieldValue reflect.Value) (reflect.Value, error) { v := reflect.ValueOf(attribute) values := make([]string, v.Len()) for i := 0; i < v.Len(); i++ { @@ -446,8 +454,11 @@ func handleStringSlice(attribute interface{}, args []string, fieldType reflect.T return reflect.ValueOf(values), nil } -func handleTime(attribute interface{}, args []string, fieldType reflect.Type, fieldValue reflect.Value) (reflect.Value, error) { - +func handleTime( + attribute interface{}, + args []string, + fieldType reflect.Type, + fieldValue reflect.Value) (reflect.Value, error) { var isIso8601 bool v := reflect.ValueOf(attribute) @@ -494,7 +505,11 @@ func handleTime(attribute interface{}, args []string, fieldType reflect.Type, fi return reflect.ValueOf(t), nil } -func handleNumeric(attribute interface{}, args []string, fieldType reflect.Type, fieldValue reflect.Value) (reflect.Value, error) { +func handleNumeric( + attribute interface{}, + args []string, + fieldType reflect.Type, + fieldValue reflect.Value) (reflect.Value, error) { v := reflect.ValueOf(attribute) floatValue := v.Interface().(float64) @@ -551,7 +566,12 @@ func handleNumeric(attribute interface{}, args []string, fieldType reflect.Type, return numericValue, nil } -func handlePointer(attribute interface{}, args []string, fieldType reflect.Type, fieldValue reflect.Value, structField reflect.StructField) (reflect.Value, error) { +func handlePointer( + attribute interface{}, + args []string, + fieldType reflect.Type, + fieldValue reflect.Value, + structField reflect.StructField) (reflect.Value, error) { t := fieldValue.Type() var concreteVal reflect.Value @@ -560,50 +580,55 @@ func handlePointer(attribute interface{}, args []string, fieldType reflect.Type, concreteVal = reflect.ValueOf(&cVal) case bool: concreteVal = reflect.ValueOf(&cVal) - case complex64: - concreteVal = reflect.ValueOf(&cVal) - case complex128: - concreteVal = reflect.ValueOf(&cVal) - case uintptr: + case complex64, complex128, uintptr: concreteVal = reflect.ValueOf(&cVal) case map[string]interface{}: var err error concreteVal, err = handleStruct(attribute, args, fieldType, fieldValue) if err != nil { - return reflect.Value{}, newErrUnsupportedPtrType(reflect.ValueOf(attribute), fieldType, structField) + return reflect.Value{}, newErrUnsupportedPtrType( + reflect.ValueOf(attribute), fieldType, structField) } return concreteVal.Elem(), err default: - return reflect.Value{}, newErrUnsupportedPtrType(reflect.ValueOf(attribute), fieldType, structField) + return reflect.Value{}, newErrUnsupportedPtrType( + reflect.ValueOf(attribute), fieldType, structField) } if t != concreteVal.Type() { - return reflect.Value{}, newErrUnsupportedPtrType(reflect.ValueOf(attribute), fieldType, structField) + return reflect.Value{}, newErrUnsupportedPtrType( + reflect.ValueOf(attribute), fieldType, structField) } return concreteVal, nil } -func handleStruct(attribute interface{}, args []string, fieldType reflect.Type, fieldValue reflect.Value) (reflect.Value, error) { +func handleStruct( + attribute interface{}, + args []string, + fieldType reflect.Type, + fieldValue reflect.Value) (reflect.Value, error) { model := reflect.New(fieldValue.Type()) - var er error - - data, er := json.Marshal(attribute) - if er != nil { - return model, er + data, err := json.Marshal(attribute) + if err != nil { + return model, err } - er = json.Unmarshal(data, model.Interface()) + err = json.Unmarshal(data, model.Interface()) - if er != nil { - return model, er + if err != nil { + return model, err } - return model, er + return model, err } -func handleStructSlice(attribute interface{}, args []string, fieldType reflect.Type, fieldValue reflect.Value) (reflect.Value, error) { +func handleStructSlice( + attribute interface{}, + args []string, + fieldType reflect.Type, + fieldValue reflect.Value) (reflect.Value, error) { models := reflect.New(fieldValue.Type()).Elem() dataMap := reflect.ValueOf(attribute).Interface().([]interface{}) for _, data := range dataMap { From 3c8221b3737093967211b231b2a7a3dadb16256d Mon Sep 17 00:00:00 2001 From: Sam Woodard Date: Fri, 28 Sep 2018 14:57:05 -0700 Subject: [PATCH 17/17] use jsonapi tags for nested attrs --- models_test.go | 14 +++---- request.go | 99 ++++++++++++++++--------------------------------- request_test.go | 31 ++++++++-------- 3 files changed, 54 insertions(+), 90 deletions(-) diff --git a/models_test.go b/models_test.go index eb38c98..d443378 100644 --- a/models_test.go +++ b/models_test.go @@ -165,14 +165,14 @@ type Company struct { } type Team struct { - Name string `json:"name"` - Leader *Employee `json:"leader"` - Members []Employee `json:"members"` + Name string `jsonapi:"attr,name"` + Leader *Employee `jsonapi:"attr,leader"` + Members []Employee `jsonapi:"attr,members"` } type Employee struct { - Firstname string `json:"firstname"` - Surname string `json:"surname"` - Age int `json:"age"` - HiredAt *time.Time `json:"hired-at,iso8601"` + Firstname string `jsonapi:"attr,firstname"` + Surname string `jsonapi:"attr,surname"` + Age int `jsonapi:"attr,age"` + HiredAt *time.Time `jsonapi:"attr,hired-at,iso8601"` } diff --git a/request.go b/request.go index ff426ce..b9883f2 100644 --- a/request.go +++ b/request.go @@ -13,7 +13,7 @@ import ( ) const ( - unsuportedStructTagMsg = "Unsupported jsonapi tag annotation, %s" + unsupportedStructTagMsg = "Unsupported jsonapi tag annotation, %s" ) var ( @@ -147,7 +147,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) }() modelValue := model.Elem() - modelType := model.Type().Elem() + modelType := modelValue.Type() var er error @@ -217,39 +217,8 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) // Convert the numeric float to one of the supported ID numeric types // (int[8,16,32,64] or uint[8,16,32,64]) - var idValue reflect.Value - switch kind { - case reflect.Int: - n := int(floatValue) - idValue = reflect.ValueOf(&n) - case reflect.Int8: - n := int8(floatValue) - idValue = reflect.ValueOf(&n) - case reflect.Int16: - n := int16(floatValue) - idValue = reflect.ValueOf(&n) - case reflect.Int32: - n := int32(floatValue) - idValue = reflect.ValueOf(&n) - case reflect.Int64: - n := int64(floatValue) - idValue = reflect.ValueOf(&n) - case reflect.Uint: - n := uint(floatValue) - idValue = reflect.ValueOf(&n) - case reflect.Uint8: - n := uint8(floatValue) - idValue = reflect.ValueOf(&n) - case reflect.Uint16: - n := uint16(floatValue) - idValue = reflect.ValueOf(&n) - case reflect.Uint32: - n := uint32(floatValue) - idValue = reflect.ValueOf(&n) - case reflect.Uint64: - n := uint64(floatValue) - idValue = reflect.ValueOf(&n) - default: + idValue, err := handleNumeric(floatValue, fieldType.Type, fieldValue) + if err != nil { // We had a JSON float (numeric), but our field was not one of the // allowed numeric types er = ErrBadJSONAPIID @@ -358,7 +327,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) } } else { - er = fmt.Errorf(unsuportedStructTagMsg, annotation) + er = fmt.Errorf(unsupportedStructTagMsg, annotation) } } @@ -395,33 +364,33 @@ func unmarshalAttribute( // Handle field of type []string if fieldValue.Type() == reflect.TypeOf([]string{}) { - value, err = handleStringSlice(attribute, args, fieldType, fieldValue) + value, err = handleStringSlice(attribute) return } // Handle field of type time.Time if fieldValue.Type() == reflect.TypeOf(time.Time{}) || fieldValue.Type() == reflect.TypeOf(new(time.Time)) { - value, err = handleTime(attribute, args, fieldType, fieldValue) + value, err = handleTime(attribute, args, fieldValue) return } // Handle field of type struct if fieldValue.Type().Kind() == reflect.Struct { - value, err = handleStruct(attribute, args, fieldType, fieldValue) + value, err = handleStruct(attribute, fieldValue) return } // Handle field containing slice of structs if fieldValue.Type().Kind() == reflect.Slice && reflect.TypeOf(fieldValue.Interface()).Elem().Kind() == reflect.Struct { - value, err = handleStructSlice(attribute, args, fieldType, fieldValue) + value, err = handleStructSlice(attribute, fieldValue) return } // JSON value was a float (numeric) if value.Kind() == reflect.Float64 { - value, err = handleNumeric(attribute, args, fieldType, fieldValue) + value, err = handleNumeric(attribute, fieldType, fieldValue) return } @@ -440,11 +409,7 @@ func unmarshalAttribute( return } -func handleStringSlice( - attribute interface{}, - args []string, - fieldType reflect.Type, - fieldValue reflect.Value) (reflect.Value, error) { +func handleStringSlice(attribute interface{}) (reflect.Value, error) { v := reflect.ValueOf(attribute) values := make([]string, v.Len()) for i := 0; i < v.Len(); i++ { @@ -454,11 +419,7 @@ func handleStringSlice( return reflect.ValueOf(values), nil } -func handleTime( - attribute interface{}, - args []string, - fieldType reflect.Type, - fieldValue reflect.Value) (reflect.Value, error) { +func handleTime(attribute interface{}, args []string, fieldValue reflect.Value) (reflect.Value, error) { var isIso8601 bool v := reflect.ValueOf(attribute) @@ -507,7 +468,6 @@ func handleTime( func handleNumeric( attribute interface{}, - args []string, fieldType reflect.Type, fieldValue reflect.Value) (reflect.Value, error) { v := reflect.ValueOf(attribute) @@ -584,12 +544,12 @@ func handlePointer( concreteVal = reflect.ValueOf(&cVal) case map[string]interface{}: var err error - concreteVal, err = handleStruct(attribute, args, fieldType, fieldValue) + concreteVal, err = handleStruct(attribute, fieldValue) if err != nil { return reflect.Value{}, newErrUnsupportedPtrType( reflect.ValueOf(attribute), fieldType, structField) } - return concreteVal.Elem(), err + return concreteVal, err default: return reflect.Value{}, newErrUnsupportedPtrType( reflect.ValueOf(attribute), fieldType, structField) @@ -605,37 +565,42 @@ func handlePointer( func handleStruct( attribute interface{}, - args []string, - fieldType reflect.Type, fieldValue reflect.Value) (reflect.Value, error) { - model := reflect.New(fieldValue.Type()) data, err := json.Marshal(attribute) if err != nil { - return model, err + return reflect.Value{}, err } - err = json.Unmarshal(data, model.Interface()) - - if err != nil { - return model, err + node := new(Node) + if err := json.Unmarshal(data, &node.Attributes); err != nil { + return reflect.Value{}, err } - return model, err + var model reflect.Value + if fieldValue.Kind() == reflect.Ptr { + model = reflect.New(fieldValue.Type().Elem()) + } else { + model = reflect.New(fieldValue.Type()) + } + + if err := unmarshalNode(node, model, nil); err != nil { + return reflect.Value{}, err + } + + + return model, nil } func handleStructSlice( attribute interface{}, - args []string, - fieldType reflect.Type, fieldValue reflect.Value) (reflect.Value, error) { models := reflect.New(fieldValue.Type()).Elem() dataMap := reflect.ValueOf(attribute).Interface().([]interface{}) for _, data := range dataMap { model := reflect.New(fieldValue.Type().Elem()).Elem() - modelType := model.Type() - value, err := handleStruct(data, []string{}, modelType, model) + value, err := handleStruct(data, model) if err != nil { continue diff --git a/request_test.go b/request_test.go index 8ee4a5a..111b5fb 100644 --- a/request_test.go +++ b/request_test.go @@ -1013,8 +1013,8 @@ func sampleSerializedEmbeddedTestModel() *Blog { func TestUnmarshalNestedStructPtr(t *testing.T) { type Director struct { - Firstname string `json:"firstname"` - Surname string `json:"surname"` + Firstname string `jsonapi:"attr,firstname"` + Surname string `jsonapi:"attr,surname"` } type Movie struct { ID string `jsonapi:"primary,movies"` @@ -1058,7 +1058,6 @@ func TestUnmarshalNestedStructPtr(t *testing.T) { } func TestUnmarshalNestedStruct(t *testing.T) { - boss := map[string]interface{}{ "firstname": "Hubert", "surname": "Farnsworth", @@ -1074,22 +1073,22 @@ func TestUnmarshalNestedStruct(t *testing.T) { "name": "Planet Express", "boss": boss, "founded-at": "2016-08-17T08:27:12Z", - "teams": []Team{ - Team{ - Name: "Dev", - Members: []Employee{ - Employee{Firstname: "Sean"}, - Employee{Firstname: "Iz"}, + "teams": []map[string]interface{}{ + map[string]interface{}{ + "name": "Dev", + "members": []map[string]interface{}{ + map[string]interface{}{"firstname": "Sean"}, + map[string]interface{}{"firstname": "Iz"}, }, - Leader: &Employee{Firstname: "Iz"}, + "leader": map[string]interface{}{"firstname": "Iz"}, }, - Team{ - Name: "DxE", - Members: []Employee{ - Employee{Firstname: "Akshay"}, - Employee{Firstname: "Peri"}, + map[string]interface{}{ + "name": "DxE", + "members": []map[string]interface{}{ + map[string]interface{}{"firstname": "Akshay"}, + map[string]interface{}{"firstname": "Peri"}, }, - Leader: &Employee{Firstname: "Peri"}, + "leader": map[string]interface{}{"firstname": "Peri"}, }, }, },