From 5836d603a39586ae048639f5655198cab0d3798d Mon Sep 17 00:00:00 2001 From: Anthony Dodd Date: Mon, 18 Jul 2016 16:48:35 -0500 Subject: [PATCH 1/2] Handle type errors properly during unmarshal node. During the `unmarshalNode` routine, various type errors may come up. Currently they are being swallowed by the error handling mechanism. Rework that system so that errors are actually exposed and can be corrected without having to dive into the code. Handle time.Time & *time.Time type errors. Add test for unmarshalling ptr with bad type. Final `attr` type check catch-all & tests. Implement system for JSON API compatible errors. This commit creates a series of structs and interfaces for representing JSON API compatible error objects. It includes a method for emitting a payload of error objects (according to the JSON API spec). Its algorithm is based upon a series of interfaces which define incremental levels of support for errors which are JSON API compatible. Use new error type for unmarshalling errors. Update some docs & the privacy of a func. Don't be prescriptive on `meta`. Use map[string]interface{}. Add a README section on errors &c. Do not force inclusion of `title` & `detail` (use omitempty). Update README & docs on use of pertient errors related types. Update tests to account for these changes. If error type is already *ErrorObject, return immediately. Rm everything except for ErrorsPayload, ErrorObject & MarshalErrors. Also update README & tests. Remove a test fixture which is no longer needed. --- README.md | 48 ++++++++++++++++++++++++++++++ errors.go | 55 ++++++++++++++++++++++++++++++++++ errors_test.go | 52 +++++++++++++++++++++++++++++++++ request.go | 69 ++++++++++++++++++++++++++----------------- request_test.go | 78 ++++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 274 insertions(+), 28 deletions(-) create mode 100644 errors.go create mode 100644 errors_test.go diff --git a/README.md b/README.md index 7efab89..98d481b 100644 --- a/README.md +++ b/README.md @@ -359,6 +359,54 @@ func (post Post) JSONAPIRelationshipLinks(relation string) *map[string]interface } ``` +### Errors +This package also implements support for JSON API compatible `errors` payloads using the following types. + +#### `MarshalErrors` +```go +MarshalErrors(w io.Writer, errs []*ErrorObject) error +``` + +Writes a JSON API response using the given `[]error`. + +#### `ErrorsPayload` +```go +type ErrorsPayload struct { + Errors []*ErrorObject `json:"errors"` +} +``` + +ErrorsPayload is a serializer struct for representing a valid JSON API errors payload. + +#### `ErrorObject` +```go +type ErrorObject struct { ... } + +// Error implements the `Error` interface. +func (e *ErrorObject) Error() string { + return fmt.Sprintf("Error: %s %s\n", e.Title, e.Detail) +} +``` + +ErrorObject is an `Error` implementation as well as an implementation of the JSON API error object. + +The main idea behind this struct is that you can use it directly in your code as an error type and pass it directly to `MarshalErrors` to get a valid JSON API errors payload. + +##### Errors Example Code +```go +// An error has come up in your code, so set an appropriate status, and serialize the error. +if err := validate(&myStructToValidate); err != nil { + context.SetStatusCode(http.StatusBadRequest) // Or however you need to set a status. + jsonapi.MarshalErrors(w, []*ErrorObject{{ + Title: "Validation Error", + Detail: "Given request body was invalid.", + Status: "400", + Meta: map[string]interface{}{"field": "some_field", "error": "bad type", "expected": "string", "received": "float64"}, + }}) + return +} +``` + ## Testing ### `MarshalOnePayloadEmbedded` diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..ed7fa9f --- /dev/null +++ b/errors.go @@ -0,0 +1,55 @@ +package jsonapi + +import ( + "encoding/json" + "fmt" + "io" +) + +// MarshalErrors writes a JSON API response using the given `[]error`. +// +// For more information on JSON API error payloads, see the spec here: +// http://jsonapi.org/format/#document-top-level +// and here: http://jsonapi.org/format/#error-objects. +func MarshalErrors(w io.Writer, errorObjects []*ErrorObject) error { + if err := json.NewEncoder(w).Encode(&ErrorsPayload{Errors: errorObjects}); err != nil { + return err + } + return nil +} + +// ErrorsPayload is a serializer struct for representing a valid JSON API errors payload. +type ErrorsPayload struct { + Errors []*ErrorObject `json:"errors"` +} + +// ErrorObject is an `Error` implementation as well as an implementation of the JSON API error object. +// +// The main idea behind this struct is that you can use it directly in your code as an error type +// and pass it directly to `MarshalErrors` to get a valid JSON API errors payload. +// For more information on Golang errors, see: https://golang.org/pkg/errors/ +// For more information on the JSON API spec's error objects, see: http://jsonapi.org/format/#error-objects +type ErrorObject struct { + // ID is a unique identifier for this particular occurrence of a problem. + ID string `json:"id,omitempty"` + + // Title is a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization. + Title string `json:"title,omitempty"` + + // Detail is a human-readable explanation specific to this occurrence of the problem. Like title, this field’s value can be localized. + Detail string `json:"detail,omitempty"` + + // Status is the HTTP status code applicable to this problem, expressed as a string value. + Status string `json:"status,omitempty"` + + // Code is an application-specific error code, expressed as a string value. + Code string `json:"code,omitempty"` + + // Meta is an object containing non-standard meta-information about the error. + Meta *map[string]interface{} `json:"meta,omitempty"` +} + +// Error implements the `Error` interface. +func (e *ErrorObject) Error() string { + return fmt.Sprintf("Error: %s %s\n", e.Title, e.Detail) +} diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 0000000..e251d09 --- /dev/null +++ b/errors_test.go @@ -0,0 +1,52 @@ +package jsonapi + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "reflect" + "testing" +) + +func TestErrorObjectWritesExpectedErrorMessage(t *testing.T) { + err := &ErrorObject{Title: "Title test.", Detail: "Detail test."} + var input error = err + + output := input.Error() + + if output != fmt.Sprintf("Error: %s %s\n", err.Title, err.Detail) { + t.Fatal("Unexpected output.") + } +} + +func TestMarshalErrorsWritesTheExpectedPayload(t *testing.T) { + var marshalErrorsTableTasts = []struct { + In []*ErrorObject + Out map[string]interface{} + }{ + { // This tests that given fields are turned into the appropriate JSON representation. + In: []*ErrorObject{{ID: "0", Title: "Test title.", Detail: "Test detail", Status: "400", Code: "E1100"}}, + Out: map[string]interface{}{"errors": []interface{}{ + map[string]interface{}{"id": "0", "title": "Test title.", "detail": "Test detail", "status": "400", "code": "E1100"}, + }}, + }, + { // This tests that the `Meta` field is serialized properly. + In: []*ErrorObject{{Title: "Test title.", Detail: "Test detail", Meta: &map[string]interface{}{"key": "val"}}}, + Out: map[string]interface{}{"errors": []interface{}{ + map[string]interface{}{"title": "Test title.", "detail": "Test detail", "meta": map[string]interface{}{"key": "val"}}, + }}, + }, + } + for _, testRow := range marshalErrorsTableTasts { + buffer, output := bytes.NewBuffer(nil), map[string]interface{}{} + var writer io.Writer = buffer + + _ = MarshalErrors(writer, testRow.In) + json.Unmarshal(buffer.Bytes(), &output) + + if !reflect.DeepEqual(output, testRow.Out) { + t.Fatalf("Expected: \n%#v \nto equal: \n%#v", output, testRow.Out) + } + } +} diff --git a/request.go b/request.go index 4639024..fd7fbc0 100644 --- a/request.go +++ b/request.go @@ -13,6 +13,8 @@ import ( ) const ( + invalidTypeErrorTitle = "Invalid Type" + invalidTypeErrorDetail = "Invalid type encountered while unmarshalling." unsuportedStructTagMsg = "Unsupported jsonapi tag annotation, %s" ) @@ -23,13 +25,6 @@ var ( // ErrInvalidISO8601 is returned when a struct has a time.Time type field and includes // "iso8601" in the tag spec, but the JSON value was not an ISO8601 timestamp string. ErrInvalidISO8601 = errors.New("Only strings can be parsed as dates, ISO8601 timestamps") - // ErrUnknownFieldNumberType is returned when the JSON value was a float - // (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") ) // UnmarshalPayload converts an io into a struct instance using jsonapi tags on @@ -294,8 +289,12 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) } else if v.Kind() == reflect.Int { at = v.Int() } else { - er = ErrInvalidTime - break + // Return error immediately to ensure a runtime panic doesn't swallow it. + return &ErrorObject{ + Title: invalidTypeErrorTitle, + Detail: invalidTypeErrorDetail, + Meta: &map[string]interface{}{"field": args[1], "received": v.Kind().String(), "expected": reflect.Int64.String()}, + } } t := time.Unix(at, 0) @@ -346,8 +345,12 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) } else if v.Kind() == reflect.Int { at = v.Int() } else { - er = ErrInvalidTime - break + // Return error immediately to ensure a runtime panic doesn't swallow it. + return &ErrorObject{ + Title: invalidTypeErrorTitle, + Detail: invalidTypeErrorDetail, + Meta: &map[string]interface{}{"field": args[1], "received": v.Kind().String(), "expected": reflect.Int64.String()}, + } } v := time.Unix(at, 0) @@ -408,13 +411,15 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) n := float32(floatValue) numericValue = reflect.ValueOf(&n) case reflect.Float64: - n := float64(floatValue) + n := floatValue numericValue = reflect.ValueOf(&n) default: - // We had a JSON float (numeric), but our field was a non numeric - // type - er = ErrUnknownFieldNumberType - break + // Return error immediately to ensure a runtime panic doesn't swallow it. + return &ErrorObject{ + Title: invalidTypeErrorTitle, + Detail: invalidTypeErrorDetail, + Meta: &map[string]interface{}{"field": args[1], "received": reflect.Float64.String(), "expected": kind.String()}, + } } assign(fieldValue, numericValue) @@ -437,21 +442,35 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) case uintptr: concreteVal = reflect.ValueOf(&cVal) default: - er = ErrUnsupportedPtrType - break + // Return error immediately to ensure a runtime panic doesn't swallow it. + return &ErrorObject{ + Title: invalidTypeErrorTitle, + Detail: invalidTypeErrorDetail, + Meta: &map[string]interface{}{"field": args[1], "received": v.Kind().String(), "expected": fieldType.Type.Elem().String()}, + } } if fieldValue.Type() != concreteVal.Type() { - // TODO: use fmt.Errorf so that you can have a more informative - // message that reports the attempted type that was not supported. - er = ErrUnsupportedPtrType - break + // Return error immediately to ensure a runtime panic doesn't swallow it. + return &ErrorObject{ + Title: invalidTypeErrorTitle, + Detail: invalidTypeErrorDetail, + Meta: &map[string]interface{}{"field": args[1], "received": v.Kind().String(), "expected": fieldType.Type.Elem().String()}, + } } fieldValue.Set(concreteVal) continue } + // As a final catch-all, ensure types line up to avoid a runtime panic. + if fieldValue.Kind() != v.Kind() { + return &ErrorObject{ + Title: invalidTypeErrorTitle, + Detail: invalidTypeErrorDetail, + Meta: &map[string]interface{}{"field": args[1], "received": v.Kind().String(), "expected": fieldValue.Kind().String()}, + } + } fieldValue.Set(reflect.ValueOf(val)) } else if annotation == annotationRelation { @@ -529,11 +548,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) } } - if er != nil { - return er - } - - return nil + return er } func fullNode(n *Node, included *map[string]*Node) *Node { diff --git a/request_test.go b/request_test.go index 4670bfc..bfdcaec 100644 --- a/request_test.go +++ b/request_test.go @@ -3,6 +3,7 @@ package jsonapi import ( "bytes" "encoding/json" + "fmt" "io" "reflect" "strings" @@ -22,6 +23,14 @@ type WithPointer struct { FloatVal *float32 `jsonapi:"attr,float-val"` } +type ModelBadTypes struct { + ID string `jsonapi:"primary,badtypes"` + StringField string `jsonapi:"attr,string_field"` + FloatField float64 `jsonapi:"attr,float_field"` + TimeField time.Time `jsonapi:"attr,time_field"` + TimePtrField *time.Time `jsonapi:"attr,time_ptr_field"` +} + func TestUnmarshalToStructWithPointerAttr(t *testing.T) { out := new(WithPointer) in := map[string]interface{}{ @@ -36,7 +45,7 @@ func TestUnmarshalToStructWithPointerAttr(t *testing.T) { if *out.Name != "The name" { t.Fatalf("Error unmarshalling to string ptr") } - if *out.IsActive != true { + if !*out.IsActive { t.Fatalf("Error unmarshalling to bool ptr") } if *out.IntVal != 8 { @@ -98,6 +107,27 @@ func TestUnmarshalPayloadWithPointerAttr_AbsentVal(t *testing.T) { } } +func TestUnmarshalToStructWithPointerAttr_BadType(t *testing.T) { + out := new(WithPointer) + in := map[string]interface{}{ + "name": true, // This is the wrong type. + } + expectedError := &ErrorObject{Title: invalidTypeErrorTitle, Detail: invalidTypeErrorDetail, Meta: &map[string]interface{}{"field": "name", "received": "bool", "expected": "string"}} + expectedErrorMessage := fmt.Sprintf("Error: %s %s\n", expectedError.Title, expectedError.Detail) + + 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()) + } + if e, ok := err.(*ErrorObject); !ok || !reflect.DeepEqual(e, expectedError) { + t.Fatalf("Unexpected error type.") + } +} + func TestStringPointerField(t *testing.T) { // Build Book payload description := "Hello World!" @@ -150,6 +180,37 @@ func TestUnmarshalInvalidJSON(t *testing.T) { } } +func TestUnmarshalInvalidJSON_BadType(t *testing.T) { + var badTypeTests = []struct { + Field string + BadValue interface{} + Error *ErrorObject + }{ // The `Field` values here correspond to the `ModelBadTypes` jsonapi fields. + {Field: "string_field", BadValue: 0, Error: &ErrorObject{Title: invalidTypeErrorTitle, Detail: invalidTypeErrorDetail, Meta: &map[string]interface{}{"field": "string_field", "received": "float64", "expected": "string"}}}, + {Field: "float_field", BadValue: "A string.", Error: &ErrorObject{Title: invalidTypeErrorTitle, Detail: invalidTypeErrorDetail, Meta: &map[string]interface{}{"field": "float_field", "received": "string", "expected": "float64"}}}, + {Field: "time_field", BadValue: "A string.", Error: &ErrorObject{Title: invalidTypeErrorTitle, Detail: invalidTypeErrorDetail, Meta: &map[string]interface{}{"field": "time_field", "received": "string", "expected": "int64"}}}, + {Field: "time_ptr_field", BadValue: "A string.", Error: &ErrorObject{Title: invalidTypeErrorTitle, Detail: invalidTypeErrorDetail, Meta: &map[string]interface{}{"field": "time_ptr_field", "received": "string", "expected": "int64"}}}, + } + for _, test := range badTypeTests { + out := new(ModelBadTypes) + in := map[string]interface{}{} + in[test.Field] = test.BadValue + expectedErrorMessage := fmt.Sprintf("Error: %s %s\n", test.Error.Title, test.Error.Detail) + + err := UnmarshalPayload(samplePayloadWithBadTypes(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()) + } + if e, ok := err.(*ErrorObject); !ok || !reflect.DeepEqual(e, test.Error) { + t.Fatalf("Expected:\n%#v%#v\nto equal:\n%#v%#v", e, *e.Meta, test.Error, *test.Error.Meta) + } + } +} + func TestUnmarshalSetsID(t *testing.T) { in := samplePayloadWithID() out := new(Blog) @@ -759,6 +820,21 @@ func samplePayloadWithID() io.Reader { return out } +func samplePayloadWithBadTypes(m map[string]interface{}) io.Reader { + payload := &OnePayload{ + Data: &Node{ + ID: "2", + Type: "badtypes", + Attributes: m, + }, + } + + out := bytes.NewBuffer(nil) + json.NewEncoder(out).Encode(payload) + + return out +} + func sampleWithPointerPayload(m map[string]interface{}) io.Reader { payload := &OnePayload{ Data: &Node{ From 1e696092028b6345d518a1a55bcd28768d87e41e Mon Sep 17 00:00:00 2001 From: Anthony Dodd Date: Tue, 31 Jan 2017 13:18:51 -0600 Subject: [PATCH 2/2] Revert use of new error system in `request.go`. Update tests to account for generic `errors.New` use. --- request.go | 52 ++++++++++++++----------------------------------- request_test.go | 25 +++++++++--------------- 2 files changed, 24 insertions(+), 53 deletions(-) diff --git a/request.go b/request.go index fd7fbc0..af300a2 100644 --- a/request.go +++ b/request.go @@ -13,8 +13,6 @@ import ( ) const ( - invalidTypeErrorTitle = "Invalid Type" - invalidTypeErrorDetail = "Invalid type encountered while unmarshalling." unsuportedStructTagMsg = "Unsupported jsonapi tag annotation, %s" ) @@ -25,6 +23,15 @@ var ( // ErrInvalidISO8601 is returned when a struct has a time.Time type field and includes // "iso8601" in the tag spec, but the JSON value was not an ISO8601 timestamp string. ErrInvalidISO8601 = errors.New("Only strings can be parsed as dates, ISO8601 timestamps") + // ErrUnknownFieldNumberType is returned when the JSON value was a float + // (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. ) // UnmarshalPayload converts an io into a struct instance using jsonapi tags on @@ -289,12 +296,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) } else if v.Kind() == reflect.Int { at = v.Int() } else { - // Return error immediately to ensure a runtime panic doesn't swallow it. - return &ErrorObject{ - Title: invalidTypeErrorTitle, - Detail: invalidTypeErrorDetail, - Meta: &map[string]interface{}{"field": args[1], "received": v.Kind().String(), "expected": reflect.Int64.String()}, - } + return ErrInvalidTime } t := time.Unix(at, 0) @@ -345,12 +347,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) } else if v.Kind() == reflect.Int { at = v.Int() } else { - // Return error immediately to ensure a runtime panic doesn't swallow it. - return &ErrorObject{ - Title: invalidTypeErrorTitle, - Detail: invalidTypeErrorDetail, - Meta: &map[string]interface{}{"field": args[1], "received": v.Kind().String(), "expected": reflect.Int64.String()}, - } + return ErrInvalidTime } v := time.Unix(at, 0) @@ -414,12 +411,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) n := floatValue numericValue = reflect.ValueOf(&n) default: - // Return error immediately to ensure a runtime panic doesn't swallow it. - return &ErrorObject{ - Title: invalidTypeErrorTitle, - Detail: invalidTypeErrorDetail, - Meta: &map[string]interface{}{"field": args[1], "received": reflect.Float64.String(), "expected": kind.String()}, - } + return ErrUnknownFieldNumberType } assign(fieldValue, numericValue) @@ -442,21 +434,11 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) case uintptr: concreteVal = reflect.ValueOf(&cVal) default: - // Return error immediately to ensure a runtime panic doesn't swallow it. - return &ErrorObject{ - Title: invalidTypeErrorTitle, - Detail: invalidTypeErrorDetail, - Meta: &map[string]interface{}{"field": args[1], "received": v.Kind().String(), "expected": fieldType.Type.Elem().String()}, - } + return ErrUnsupportedPtrType } if fieldValue.Type() != concreteVal.Type() { - // Return error immediately to ensure a runtime panic doesn't swallow it. - return &ErrorObject{ - Title: invalidTypeErrorTitle, - Detail: invalidTypeErrorDetail, - Meta: &map[string]interface{}{"field": args[1], "received": v.Kind().String(), "expected": fieldType.Type.Elem().String()}, - } + return ErrUnsupportedPtrType } fieldValue.Set(concreteVal) @@ -465,11 +447,7 @@ 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() { - return &ErrorObject{ - Title: invalidTypeErrorTitle, - Detail: invalidTypeErrorDetail, - Meta: &map[string]interface{}{"field": args[1], "received": v.Kind().String(), "expected": fieldValue.Kind().String()}, - } + return ErrInvalidType } fieldValue.Set(reflect.ValueOf(val)) diff --git a/request_test.go b/request_test.go index bfdcaec..4fcb3b6 100644 --- a/request_test.go +++ b/request_test.go @@ -3,7 +3,6 @@ package jsonapi import ( "bytes" "encoding/json" - "fmt" "io" "reflect" "strings" @@ -112,8 +111,7 @@ func TestUnmarshalToStructWithPointerAttr_BadType(t *testing.T) { in := map[string]interface{}{ "name": true, // This is the wrong type. } - expectedError := &ErrorObject{Title: invalidTypeErrorTitle, Detail: invalidTypeErrorDetail, Meta: &map[string]interface{}{"field": "name", "received": "bool", "expected": "string"}} - expectedErrorMessage := fmt.Sprintf("Error: %s %s\n", expectedError.Title, expectedError.Detail) + expectedErrorMessage := ErrUnsupportedPtrType.Error() err := UnmarshalPayload(sampleWithPointerPayload(in), out) @@ -123,9 +121,6 @@ func TestUnmarshalToStructWithPointerAttr_BadType(t *testing.T) { if err.Error() != expectedErrorMessage { t.Fatalf("Unexpected error message: %s", err.Error()) } - if e, ok := err.(*ErrorObject); !ok || !reflect.DeepEqual(e, expectedError) { - t.Fatalf("Unexpected error type.") - } } func TestStringPointerField(t *testing.T) { @@ -184,18 +179,19 @@ func TestUnmarshalInvalidJSON_BadType(t *testing.T) { var badTypeTests = []struct { Field string BadValue interface{} - Error *ErrorObject + Error error }{ // The `Field` values here correspond to the `ModelBadTypes` jsonapi fields. - {Field: "string_field", BadValue: 0, Error: &ErrorObject{Title: invalidTypeErrorTitle, Detail: invalidTypeErrorDetail, Meta: &map[string]interface{}{"field": "string_field", "received": "float64", "expected": "string"}}}, - {Field: "float_field", BadValue: "A string.", Error: &ErrorObject{Title: invalidTypeErrorTitle, Detail: invalidTypeErrorDetail, Meta: &map[string]interface{}{"field": "float_field", "received": "string", "expected": "float64"}}}, - {Field: "time_field", BadValue: "A string.", Error: &ErrorObject{Title: invalidTypeErrorTitle, Detail: invalidTypeErrorDetail, Meta: &map[string]interface{}{"field": "time_field", "received": "string", "expected": "int64"}}}, - {Field: "time_ptr_field", BadValue: "A string.", Error: &ErrorObject{Title: invalidTypeErrorTitle, Detail: invalidTypeErrorDetail, Meta: &map[string]interface{}{"field": "time_ptr_field", "received": "string", "expected": "int64"}}}, + {Field: "string_field", BadValue: 0, Error: ErrUnknownFieldNumberType}, // Expected string. + {Field: "float_field", BadValue: "A string.", Error: ErrInvalidType}, // Expected float64. + {Field: "time_field", BadValue: "A string.", Error: ErrInvalidTime}, // Expected int64. + {Field: "time_ptr_field", BadValue: "A string.", Error: ErrInvalidTime}, // Expected *time / int64. } - for _, test := range badTypeTests { + for idx, test := range badTypeTests { + println("index:", idx) out := new(ModelBadTypes) in := map[string]interface{}{} in[test.Field] = test.BadValue - expectedErrorMessage := fmt.Sprintf("Error: %s %s\n", test.Error.Title, test.Error.Detail) + expectedErrorMessage := test.Error.Error() err := UnmarshalPayload(samplePayloadWithBadTypes(in), out) @@ -205,9 +201,6 @@ func TestUnmarshalInvalidJSON_BadType(t *testing.T) { if err.Error() != expectedErrorMessage { t.Fatalf("Unexpected error message: %s", err.Error()) } - if e, ok := err.(*ErrorObject); !ok || !reflect.DeepEqual(e, test.Error) { - t.Fatalf("Expected:\n%#v%#v\nto equal:\n%#v%#v", e, *e.Meta, test.Error, *test.Error.Meta) - } } }