├── .gitignore ├── go.mod ├── .travis.yml ├── LICENSE ├── examples ├── fixtures.go ├── models.go ├── handler_test.go ├── handler.go └── app.go ├── errors_test.go ├── constants.go ├── errors.go ├── doc.go ├── runtime.go ├── node.go ├── models_test.go ├── response.go ├── README.md ├── request.go ├── response_test.go └── request_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | /examples/examples 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/google/jsonapi -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | arch: 3 | - amd64 4 | - ppc64le 5 | go: 6 | - 1.11.x 7 | - 1.12.x 8 | - 1.13.x 9 | - 1.14.x 10 | - 1.15.x 11 | - 1.16.x 12 | - tip 13 | script: go test ./... -v 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Google Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/fixtures.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "time" 4 | 5 | func fixtureBlogCreate(i int) *Blog { 6 | return &Blog{ 7 | ID: 1 * i, 8 | Title: "Title 1", 9 | CreatedAt: time.Now(), 10 | Posts: []*Post{ 11 | { 12 | ID: 1 * i, 13 | Title: "Foo", 14 | Body: "Bar", 15 | Comments: []*Comment{ 16 | { 17 | ID: 1 * i, 18 | Body: "foo", 19 | }, 20 | { 21 | ID: 2 * i, 22 | Body: "bar", 23 | }, 24 | }, 25 | }, 26 | { 27 | ID: 2 * i, 28 | Title: "Fuubar", 29 | Body: "Bas", 30 | Comments: []*Comment{ 31 | { 32 | ID: 1 * i, 33 | Body: "foo", 34 | }, 35 | { 36 | ID: 3 * i, 37 | Body: "bas", 38 | }, 39 | }, 40 | }, 41 | }, 42 | CurrentPost: &Post{ 43 | ID: 1 * i, 44 | Title: "Foo", 45 | Body: "Bar", 46 | Comments: []*Comment{ 47 | { 48 | ID: 1 * i, 49 | Body: "foo", 50 | }, 51 | { 52 | ID: 2 * i, 53 | Body: "bar", 54 | }, 55 | }, 56 | }, 57 | } 58 | } 59 | 60 | func fixtureBlogsList() (blogs []interface{}) { 61 | for i := 0; i < 10; i++ { 62 | blogs = append(blogs, fixtureBlogCreate(i)) 63 | } 64 | 65 | return blogs 66 | } 67 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | package jsonapi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestErrorObjectWritesExpectedErrorMessage(t *testing.T) { 13 | err := &ErrorObject{Title: "Title test.", Detail: "Detail test."} 14 | var input error = err 15 | 16 | output := input.Error() 17 | 18 | if output != fmt.Sprintf("Error: %s %s\n", err.Title, err.Detail) { 19 | t.Fatal("Unexpected output.") 20 | } 21 | } 22 | 23 | func TestMarshalErrorsWritesTheExpectedPayload(t *testing.T) { 24 | var marshalErrorsTableTasts = []struct { 25 | Title string 26 | In []*ErrorObject 27 | Out map[string]interface{} 28 | }{ 29 | { 30 | Title: "TestFieldsAreSerializedAsNeeded", 31 | In: []*ErrorObject{{ID: "0", Title: "Test title.", Detail: "Test detail", Status: "400", Code: "E1100"}}, 32 | Out: map[string]interface{}{"errors": []interface{}{ 33 | map[string]interface{}{"id": "0", "title": "Test title.", "detail": "Test detail", "status": "400", "code": "E1100"}, 34 | }}, 35 | }, 36 | { 37 | Title: "TestMetaFieldIsSerializedProperly", 38 | In: []*ErrorObject{{Title: "Test title.", Detail: "Test detail", Meta: &map[string]interface{}{"key": "val"}}}, 39 | Out: map[string]interface{}{"errors": []interface{}{ 40 | map[string]interface{}{"title": "Test title.", "detail": "Test detail", "meta": map[string]interface{}{"key": "val"}}, 41 | }}, 42 | }, 43 | } 44 | for _, testRow := range marshalErrorsTableTasts { 45 | t.Run(testRow.Title, func(t *testing.T) { 46 | buffer, output := bytes.NewBuffer(nil), map[string]interface{}{} 47 | var writer io.Writer = buffer 48 | 49 | _ = MarshalErrors(writer, testRow.In) 50 | json.Unmarshal(buffer.Bytes(), &output) 51 | 52 | if !reflect.DeepEqual(output, testRow.Out) { 53 | t.Fatalf("Expected: \n%#v \nto equal: \n%#v", output, testRow.Out) 54 | } 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | package jsonapi 2 | 3 | const ( 4 | // StructTag annotation strings 5 | annotationJSONAPI = "jsonapi" 6 | annotationPrimary = "primary" 7 | annotationClientID = "client-id" 8 | annotationAttribute = "attr" 9 | annotationRelation = "relation" 10 | annotationOmitEmpty = "omitempty" 11 | annotationISO8601 = "iso8601" 12 | annotationRFC3339 = "rfc3339" 13 | annotationSeperator = "," 14 | 15 | iso8601TimeFormat = "2006-01-02T15:04:05Z" 16 | 17 | // MediaType is the identifier for the JSON API media type 18 | // 19 | // see http://jsonapi.org/format/#document-structure 20 | MediaType = "application/vnd.api+json" 21 | 22 | // Pagination Constants 23 | // 24 | // http://jsonapi.org/format/#fetching-pagination 25 | 26 | // KeyFirstPage is the key to the links object whose value contains a link to 27 | // the first page of data 28 | KeyFirstPage = "first" 29 | // KeyLastPage is the key to the links object whose value contains a link to 30 | // the last page of data 31 | KeyLastPage = "last" 32 | // KeyPreviousPage is the key to the links object whose value contains a link 33 | // to the previous page of data 34 | KeyPreviousPage = "prev" 35 | // KeyNextPage is the key to the links object whose value contains a link to 36 | // the next page of data 37 | KeyNextPage = "next" 38 | 39 | // QueryParamPageNumber is a JSON API query parameter used in a page based 40 | // pagination strategy in conjunction with QueryParamPageSize 41 | QueryParamPageNumber = "page[number]" 42 | // QueryParamPageSize is a JSON API query parameter used in a page based 43 | // pagination strategy in conjunction with QueryParamPageNumber 44 | QueryParamPageSize = "page[size]" 45 | 46 | // QueryParamPageOffset is a JSON API query parameter used in an offset based 47 | // pagination strategy in conjunction with QueryParamPageLimit 48 | QueryParamPageOffset = "page[offset]" 49 | // QueryParamPageLimit is a JSON API query parameter used in an offset based 50 | // pagination strategy in conjunction with QueryParamPageOffset 51 | QueryParamPageLimit = "page[limit]" 52 | 53 | // QueryParamPageCursor is a JSON API query parameter used with a cursor-based 54 | // strategy 55 | QueryParamPageCursor = "page[cursor]" 56 | ) 57 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package jsonapi 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | // MarshalErrors writes a JSON API response using the given `[]error`. 10 | // 11 | // For more information on JSON API error payloads, see the spec here: 12 | // http://jsonapi.org/format/#document-top-level 13 | // and here: http://jsonapi.org/format/#error-objects. 14 | func MarshalErrors(w io.Writer, errorObjects []*ErrorObject) error { 15 | return json.NewEncoder(w).Encode(&ErrorsPayload{Errors: errorObjects}) 16 | } 17 | 18 | // ErrorsPayload is a serializer struct for representing a valid JSON API errors payload. 19 | type ErrorsPayload struct { 20 | Errors []*ErrorObject `json:"errors"` 21 | } 22 | 23 | // ErrorObject is an `Error` implementation as well as an implementation of the JSON API error object. 24 | // 25 | // The main idea behind this struct is that you can use it directly in your code as an error type 26 | // and pass it directly to `MarshalErrors` to get a valid JSON API errors payload. 27 | // For more information on Golang errors, see: https://golang.org/pkg/errors/ 28 | // For more information on the JSON API spec's error objects, see: http://jsonapi.org/format/#error-objects 29 | type ErrorObject struct { 30 | // ID is a unique identifier for this particular occurrence of a problem. 31 | ID string `json:"id,omitempty"` 32 | 33 | // 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. 34 | Title string `json:"title,omitempty"` 35 | 36 | // Detail is a human-readable explanation specific to this occurrence of the problem. Like title, this field’s value can be localized. 37 | Detail string `json:"detail,omitempty"` 38 | 39 | // Status is the HTTP status code applicable to this problem, expressed as a string value. 40 | Status string `json:"status,omitempty"` 41 | 42 | // Code is an application-specific error code, expressed as a string value. 43 | Code string `json:"code,omitempty"` 44 | 45 | // Meta is an object containing non-standard meta-information about the error. 46 | Meta *map[string]interface{} `json:"meta,omitempty"` 47 | } 48 | 49 | // Error implements the `Error` interface. 50 | func (e *ErrorObject) Error() string { 51 | return fmt.Sprintf("Error: %s %s\n", e.Title, e.Detail) 52 | } 53 | -------------------------------------------------------------------------------- /examples/models.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/google/jsonapi" 8 | ) 9 | 10 | // Blog is a model representing a blog site 11 | type Blog struct { 12 | ID int `jsonapi:"primary,blogs"` 13 | Title string `jsonapi:"attr,title"` 14 | Posts []*Post `jsonapi:"relation,posts"` 15 | CurrentPost *Post `jsonapi:"relation,current_post"` 16 | CurrentPostID int `jsonapi:"attr,current_post_id"` 17 | CreatedAt time.Time `jsonapi:"attr,created_at"` 18 | ViewCount int `jsonapi:"attr,view_count"` 19 | } 20 | 21 | // Post is a model representing a post on a blog 22 | type Post struct { 23 | ID int `jsonapi:"primary,posts"` 24 | BlogID int `jsonapi:"attr,blog_id"` 25 | Title string `jsonapi:"attr,title"` 26 | Body string `jsonapi:"attr,body"` 27 | Comments []*Comment `jsonapi:"relation,comments"` 28 | } 29 | 30 | // Comment is a model representing a user submitted comment 31 | type Comment struct { 32 | ID int `jsonapi:"primary,comments"` 33 | PostID int `jsonapi:"attr,post_id"` 34 | Body string `jsonapi:"attr,body"` 35 | } 36 | 37 | // JSONAPILinks implements the Linkable interface for a blog 38 | func (blog Blog) JSONAPILinks() *jsonapi.Links { 39 | return &jsonapi.Links{ 40 | "self": fmt.Sprintf("https://example.com/blogs/%d", blog.ID), 41 | } 42 | } 43 | 44 | // JSONAPIRelationshipLinks implements the RelationshipLinkable interface for a blog 45 | func (blog Blog) JSONAPIRelationshipLinks(relation string) *jsonapi.Links { 46 | if relation == "posts" { 47 | return &jsonapi.Links{ 48 | "related": fmt.Sprintf("https://example.com/blogs/%d/posts", blog.ID), 49 | } 50 | } 51 | if relation == "current_post" { 52 | return &jsonapi.Links{ 53 | "related": fmt.Sprintf("https://example.com/blogs/%d/current_post", blog.ID), 54 | } 55 | } 56 | return nil 57 | } 58 | 59 | // JSONAPIMeta implements the Metable interface for a blog 60 | func (blog Blog) JSONAPIMeta() *jsonapi.Meta { 61 | return &jsonapi.Meta{ 62 | "detail": "extra details regarding the blog", 63 | } 64 | } 65 | 66 | // JSONAPIRelationshipMeta implements the RelationshipMetable interface for a blog 67 | func (blog Blog) JSONAPIRelationshipMeta(relation string) *jsonapi.Meta { 68 | if relation == "posts" { 69 | return &jsonapi.Meta{ 70 | "detail": "posts meta information", 71 | } 72 | } 73 | if relation == "current_post" { 74 | return &jsonapi.Meta{ 75 | "detail": "current post meta information", 76 | } 77 | } 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package jsonapi provides a serializer and deserializer for jsonapi.org spec payloads. 3 | 4 | You can keep your model structs as is and use struct field tags to indicate to jsonapi 5 | how you want your response built or your request deserialized. What about my relationships? 6 | jsonapi supports relationships out of the box and will even side load them in your response 7 | into an "included" array--that contains associated objects. 8 | 9 | jsonapi uses StructField tags to annotate the structs fields that you already have and use 10 | in your app and then reads and writes jsonapi.org output based on the instructions you give 11 | the library in your jsonapi tags. 12 | 13 | Example structs using a Blog > Post > Comment structure, 14 | 15 | type Blog struct { 16 | ID int `jsonapi:"primary,blogs"` 17 | Title string `jsonapi:"attr,title"` 18 | Posts []*Post `jsonapi:"relation,posts"` 19 | CurrentPost *Post `jsonapi:"relation,current_post"` 20 | CurrentPostID int `jsonapi:"attr,current_post_id"` 21 | CreatedAt time.Time `jsonapi:"attr,created_at"` 22 | ViewCount int `jsonapi:"attr,view_count"` 23 | } 24 | 25 | type Post struct { 26 | ID int `jsonapi:"primary,posts"` 27 | BlogID int `jsonapi:"attr,blog_id"` 28 | Title string `jsonapi:"attr,title"` 29 | Body string `jsonapi:"attr,body"` 30 | Comments []*Comment `jsonapi:"relation,comments"` 31 | } 32 | 33 | type Comment struct { 34 | ID int `jsonapi:"primary,comments"` 35 | PostID int `jsonapi:"attr,post_id"` 36 | Body string `jsonapi:"attr,body"` 37 | } 38 | 39 | jsonapi Tag Reference 40 | 41 | Value, primary: "primary," 42 | 43 | This indicates that this is the primary key field for this struct type. Tag 44 | value arguments are comma separated. The first argument must be, "primary", and 45 | the second must be the name that should appear in the "type" field for all data 46 | objects that represent this type of model. 47 | 48 | Value, attr: "attr,[,]" 49 | 50 | These fields' values should end up in the "attribute" hash for a record. The first 51 | argument must be, "attr', and the second should be the name for the key to display in 52 | the "attributes" hash for that record. 53 | 54 | The following extra arguments are also supported: 55 | 56 | "omitempty": excludes the fields value from the "attribute" hash. 57 | "iso8601": uses the ISO8601 timestamp format when serialising or deserialising the time.Time value. 58 | 59 | Value, relation: "relation," 60 | 61 | Relations are struct fields that represent a one-to-one or one-to-many to other structs. 62 | jsonapi will traverse the graph of relationships and marshal or unmarshal records. The first 63 | argument must be, "relation", and the second should be the name of the relationship, used as 64 | the key in the "relationships" hash for the record. 65 | 66 | Use the methods below to Marshal and Unmarshal jsonapi.org json payloads. 67 | 68 | Visit the readme at https://github.com/google/jsonapi 69 | */ 70 | package jsonapi 71 | -------------------------------------------------------------------------------- /examples/handler_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/google/jsonapi" 10 | ) 11 | 12 | func TestExampleHandler_post(t *testing.T) { 13 | blog := fixtureBlogCreate(1) 14 | requestBody := bytes.NewBuffer(nil) 15 | jsonapi.MarshalOnePayloadEmbedded(requestBody, blog) 16 | 17 | r, err := http.NewRequest(http.MethodPost, "/blogs?id=1", requestBody) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | r.Header.Set(headerAccept, jsonapi.MediaType) 22 | 23 | rr := httptest.NewRecorder() 24 | handler := &ExampleHandler{} 25 | handler.ServeHTTP(rr, r) 26 | 27 | if e, a := http.StatusCreated, rr.Code; e != a { 28 | t.Fatalf("Expected a status of %d, got %d", e, a) 29 | } 30 | } 31 | 32 | func TestExampleHandler_put(t *testing.T) { 33 | blogs := []interface{}{ 34 | fixtureBlogCreate(1), 35 | fixtureBlogCreate(2), 36 | fixtureBlogCreate(3), 37 | } 38 | requestBody := bytes.NewBuffer(nil) 39 | jsonapi.MarshalPayload(requestBody, blogs) 40 | 41 | r, err := http.NewRequest(http.MethodPut, "/blogs", requestBody) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | r.Header.Set(headerAccept, jsonapi.MediaType) 46 | 47 | rr := httptest.NewRecorder() 48 | handler := &ExampleHandler{} 49 | handler.ServeHTTP(rr, r) 50 | 51 | if e, a := http.StatusOK, rr.Code; e != a { 52 | t.Fatalf("Expected a status of %d, got %d", e, a) 53 | } 54 | } 55 | 56 | func TestExampleHandler_get_show(t *testing.T) { 57 | r, err := http.NewRequest(http.MethodGet, "/blogs?id=1", nil) 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | r.Header.Set(headerAccept, jsonapi.MediaType) 62 | 63 | rr := httptest.NewRecorder() 64 | handler := &ExampleHandler{} 65 | handler.ServeHTTP(rr, r) 66 | 67 | if e, a := http.StatusOK, rr.Code; e != a { 68 | t.Fatalf("Expected a status of %d, got %d", e, a) 69 | } 70 | } 71 | 72 | func TestExampleHandler_get_list(t *testing.T) { 73 | r, err := http.NewRequest(http.MethodGet, "/blogs", nil) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | r.Header.Set(headerAccept, jsonapi.MediaType) 78 | 79 | rr := httptest.NewRecorder() 80 | handler := &ExampleHandler{} 81 | handler.ServeHTTP(rr, r) 82 | 83 | if e, a := http.StatusOK, rr.Code; e != a { 84 | t.Fatalf("Expected a status of %d, got %d", e, a) 85 | } 86 | } 87 | 88 | func TestHttpErrorWhenHeaderDoesNotMatch(t *testing.T) { 89 | r, err := http.NewRequest(http.MethodGet, "/blogs", nil) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | r.Header.Set(headerAccept, "application/xml") 94 | 95 | rr := httptest.NewRecorder() 96 | handler := &ExampleHandler{} 97 | handler.ServeHTTP(rr, r) 98 | 99 | if rr.Code != http.StatusUnsupportedMediaType { 100 | t.Fatal("expected Unsupported Media Type staus error") 101 | } 102 | } 103 | 104 | func TestHttpErrorWhenMethodDoesNotMatch(t *testing.T) { 105 | r, err := http.NewRequest(http.MethodPatch, "/blogs", nil) 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | r.Header.Set(headerAccept, jsonapi.MediaType) 110 | 111 | rr := httptest.NewRecorder() 112 | handler := &ExampleHandler{} 113 | handler.ServeHTTP(rr, r) 114 | 115 | if rr.Code != http.StatusNotFound { 116 | t.Fatal("expected HTTP Status Not Found status error") 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /examples/handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/google/jsonapi" 8 | ) 9 | 10 | const ( 11 | headerAccept = "Accept" 12 | headerContentType = "Content-Type" 13 | ) 14 | 15 | // ExampleHandler is the handler we are using to demonstrate building an HTTP 16 | // server with the jsonapi library. 17 | type ExampleHandler struct{} 18 | 19 | func (h *ExampleHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 20 | if r.Header.Get(headerAccept) != jsonapi.MediaType { 21 | http.Error(w, "Unsupported Media Type", http.StatusUnsupportedMediaType) 22 | } 23 | 24 | var methodHandler http.HandlerFunc 25 | switch r.Method { 26 | case http.MethodPost: 27 | methodHandler = h.createBlog 28 | case http.MethodPut: 29 | methodHandler = h.echoBlogs 30 | case http.MethodGet: 31 | if r.FormValue("id") != "" { 32 | methodHandler = h.showBlog 33 | } else { 34 | methodHandler = h.listBlogs 35 | } 36 | default: 37 | http.Error(w, "Not Found", http.StatusNotFound) 38 | return 39 | } 40 | 41 | methodHandler(w, r) 42 | } 43 | 44 | func (h *ExampleHandler) createBlog(w http.ResponseWriter, r *http.Request) { 45 | jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.create") 46 | 47 | blog := new(Blog) 48 | 49 | if err := jsonapiRuntime.UnmarshalPayload(r.Body, blog); err != nil { 50 | http.Error(w, err.Error(), http.StatusInternalServerError) 51 | return 52 | } 53 | 54 | // ...do stuff with your blog... 55 | 56 | w.WriteHeader(http.StatusCreated) 57 | w.Header().Set(headerContentType, jsonapi.MediaType) 58 | 59 | if err := jsonapiRuntime.MarshalPayload(w, blog); err != nil { 60 | http.Error(w, err.Error(), http.StatusInternalServerError) 61 | } 62 | } 63 | 64 | func (h *ExampleHandler) echoBlogs(w http.ResponseWriter, r *http.Request) { 65 | jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.list") 66 | // ...fetch your blogs, filter, offset, limit, etc... 67 | 68 | // but, for now 69 | blogs := fixtureBlogsList() 70 | 71 | w.WriteHeader(http.StatusOK) 72 | w.Header().Set(headerContentType, jsonapi.MediaType) 73 | if err := jsonapiRuntime.MarshalPayload(w, blogs); err != nil { 74 | http.Error(w, err.Error(), http.StatusInternalServerError) 75 | } 76 | } 77 | 78 | func (h *ExampleHandler) showBlog(w http.ResponseWriter, r *http.Request) { 79 | id := r.FormValue("id") 80 | 81 | // ...fetch your blog... 82 | 83 | intID, err := strconv.Atoi(id) 84 | if err != nil { 85 | http.Error(w, err.Error(), http.StatusInternalServerError) 86 | return 87 | } 88 | 89 | jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.show") 90 | 91 | // but, for now 92 | blog := fixtureBlogCreate(intID) 93 | w.WriteHeader(http.StatusOK) 94 | 95 | w.Header().Set(headerContentType, jsonapi.MediaType) 96 | if err := jsonapiRuntime.MarshalPayload(w, blog); err != nil { 97 | http.Error(w, err.Error(), http.StatusInternalServerError) 98 | } 99 | } 100 | 101 | func (h *ExampleHandler) listBlogs(w http.ResponseWriter, r *http.Request) { 102 | jsonapiRuntime := jsonapi.NewRuntime().Instrument("blogs.list") 103 | // ...fetch your blogs, filter, offset, limit, etc... 104 | 105 | // but, for now 106 | blogs := fixtureBlogsList() 107 | 108 | w.Header().Set("Content-Type", jsonapi.MediaType) 109 | w.WriteHeader(http.StatusOK) 110 | 111 | if err := jsonapiRuntime.MarshalPayload(w, blogs); err != nil { 112 | http.Error(w, err.Error(), http.StatusInternalServerError) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /runtime.go: -------------------------------------------------------------------------------- 1 | package jsonapi 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "io" 7 | "reflect" 8 | "time" 9 | ) 10 | 11 | // Event represents a lifecycle event in the marshaling or unmarshalling 12 | // process. 13 | type Event int 14 | 15 | const ( 16 | // UnmarshalStart is the Event that is sent when deserialization of a payload 17 | // begins. 18 | UnmarshalStart Event = iota 19 | 20 | // UnmarshalStop is the Event that is sent when deserialization of a payload 21 | // ends. 22 | UnmarshalStop 23 | 24 | // MarshalStart is the Event that is sent sent when serialization of a payload 25 | // begins. 26 | MarshalStart 27 | 28 | // MarshalStop is the Event that is sent sent when serialization of a payload 29 | // ends. 30 | MarshalStop 31 | ) 32 | 33 | // Runtime has the same methods as jsonapi package for serialization and 34 | // deserialization but also has a ctx, a map[string]interface{} for storing 35 | // state, designed for instrumenting serialization timings. 36 | type Runtime struct { 37 | ctx map[string]interface{} 38 | } 39 | 40 | // Events is the func type that provides the callback for handling event timings. 41 | type Events func(*Runtime, Event, string, time.Duration) 42 | 43 | // Instrumentation is a a global Events variable. This is the handler for all 44 | // timing events. 45 | var Instrumentation Events 46 | 47 | // NewRuntime creates a Runtime for use in an application. 48 | func NewRuntime() *Runtime { return &Runtime{make(map[string]interface{})} } 49 | 50 | // WithValue adds custom state variables to the runtime context. 51 | func (r *Runtime) WithValue(key string, value interface{}) *Runtime { 52 | r.ctx[key] = value 53 | 54 | return r 55 | } 56 | 57 | // Value returns a state variable in the runtime context. 58 | func (r *Runtime) Value(key string) interface{} { 59 | return r.ctx[key] 60 | } 61 | 62 | // Instrument is deprecated. 63 | func (r *Runtime) Instrument(key string) *Runtime { 64 | return r.WithValue("instrument", key) 65 | } 66 | 67 | func (r *Runtime) shouldInstrument() bool { 68 | return Instrumentation != nil 69 | } 70 | 71 | // UnmarshalPayload has docs in request.go for UnmarshalPayload. 72 | func (r *Runtime) UnmarshalPayload(reader io.Reader, model interface{}) error { 73 | return r.instrumentCall(UnmarshalStart, UnmarshalStop, func() error { 74 | return UnmarshalPayload(reader, model) 75 | }) 76 | } 77 | 78 | // UnmarshalManyPayload has docs in request.go for UnmarshalManyPayload. 79 | func (r *Runtime) UnmarshalManyPayload(reader io.Reader, kind reflect.Type) (elems []interface{}, err error) { 80 | r.instrumentCall(UnmarshalStart, UnmarshalStop, func() error { 81 | elems, err = UnmarshalManyPayload(reader, kind) 82 | return err 83 | }) 84 | 85 | return 86 | } 87 | 88 | // MarshalPayload has docs in response.go for MarshalPayload. 89 | func (r *Runtime) MarshalPayload(w io.Writer, model interface{}) error { 90 | return r.instrumentCall(MarshalStart, MarshalStop, func() error { 91 | return MarshalPayload(w, model) 92 | }) 93 | } 94 | 95 | func (r *Runtime) instrumentCall(start Event, stop Event, c func() error) error { 96 | if !r.shouldInstrument() { 97 | return c() 98 | } 99 | 100 | instrumentationGUID, err := newUUID() 101 | if err != nil { 102 | return err 103 | } 104 | 105 | begin := time.Now() 106 | Instrumentation(r, start, instrumentationGUID, time.Duration(0)) 107 | 108 | if err := c(); err != nil { 109 | return err 110 | } 111 | 112 | diff := time.Duration(time.Now().UnixNano() - begin.UnixNano()) 113 | Instrumentation(r, stop, instrumentationGUID, diff) 114 | 115 | return nil 116 | } 117 | 118 | // citation: http://play.golang.org/p/4FkNSiUDMg 119 | func newUUID() (string, error) { 120 | uuid := make([]byte, 16) 121 | if _, err := io.ReadFull(rand.Reader, uuid); err != nil { 122 | return "", err 123 | } 124 | // variant bits; see section 4.1.1 125 | uuid[8] = uuid[8]&^0xc0 | 0x80 126 | // version 4 (pseudo-random); see section 4.1.3 127 | uuid[6] = uuid[6]&^0xf0 | 0x40 128 | return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]), nil 129 | } 130 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | package jsonapi 2 | 3 | import "fmt" 4 | 5 | // Payloader is used to encapsulate the One and Many payload types 6 | type Payloader interface { 7 | clearIncluded() 8 | } 9 | 10 | // OnePayload is used to represent a generic JSON API payload where a single 11 | // resource (Node) was included as an {} in the "data" key 12 | type OnePayload struct { 13 | Data *Node `json:"data"` 14 | Included []*Node `json:"included,omitempty"` 15 | Links *Links `json:"links,omitempty"` 16 | Meta *Meta `json:"meta,omitempty"` 17 | } 18 | 19 | func (p *OnePayload) clearIncluded() { 20 | p.Included = []*Node{} 21 | } 22 | 23 | // ManyPayload is used to represent a generic JSON API payload where many 24 | // resources (Nodes) were included in an [] in the "data" key 25 | type ManyPayload struct { 26 | Data []*Node `json:"data"` 27 | Included []*Node `json:"included,omitempty"` 28 | Links *Links `json:"links,omitempty"` 29 | Meta *Meta `json:"meta,omitempty"` 30 | } 31 | 32 | func (p *ManyPayload) clearIncluded() { 33 | p.Included = []*Node{} 34 | } 35 | 36 | // Node is used to represent a generic JSON API Resource 37 | type Node struct { 38 | Type string `json:"type"` 39 | ID string `json:"id,omitempty"` 40 | ClientID string `json:"client-id,omitempty"` 41 | Attributes map[string]interface{} `json:"attributes,omitempty"` 42 | Relationships map[string]interface{} `json:"relationships,omitempty"` 43 | Links *Links `json:"links,omitempty"` 44 | Meta *Meta `json:"meta,omitempty"` 45 | } 46 | 47 | // RelationshipOneNode is used to represent a generic has one JSON API relation 48 | type RelationshipOneNode struct { 49 | Data *Node `json:"data"` 50 | Links *Links `json:"links,omitempty"` 51 | Meta *Meta `json:"meta,omitempty"` 52 | } 53 | 54 | // RelationshipManyNode is used to represent a generic has many JSON API 55 | // relation 56 | type RelationshipManyNode struct { 57 | Data []*Node `json:"data"` 58 | Links *Links `json:"links,omitempty"` 59 | Meta *Meta `json:"meta,omitempty"` 60 | } 61 | 62 | // Links is used to represent a `links` object. 63 | // http://jsonapi.org/format/#document-links 64 | type Links map[string]interface{} 65 | 66 | func (l *Links) validate() (err error) { 67 | // Each member of a links object is a “link”. A link MUST be represented as 68 | // either: 69 | // - a string containing the link’s URL. 70 | // - an object (“link object”) which can contain the following members: 71 | // - href: a string containing the link’s URL. 72 | // - meta: a meta object containing non-standard meta-information about the 73 | // link. 74 | for k, v := range *l { 75 | _, isString := v.(string) 76 | _, isLink := v.(Link) 77 | 78 | if !(isString || isLink) { 79 | return fmt.Errorf( 80 | "The %s member of the links object was not a string or link object", 81 | k, 82 | ) 83 | } 84 | } 85 | return 86 | } 87 | 88 | // Link is used to represent a member of the `links` object. 89 | type Link struct { 90 | Href string `json:"href"` 91 | Meta Meta `json:"meta,omitempty"` 92 | } 93 | 94 | // Linkable is used to include document links in response data 95 | // e.g. {"self": "http://example.com/posts/1"} 96 | type Linkable interface { 97 | JSONAPILinks() *Links 98 | } 99 | 100 | // RelationshipLinkable is used to include relationship links in response data 101 | // e.g. {"related": "http://example.com/posts/1/comments"} 102 | type RelationshipLinkable interface { 103 | // JSONAPIRelationshipLinks will be invoked for each relationship with the corresponding relation name (e.g. `comments`) 104 | JSONAPIRelationshipLinks(relation string) *Links 105 | } 106 | 107 | // Meta is used to represent a `meta` object. 108 | // http://jsonapi.org/format/#document-meta 109 | type Meta map[string]interface{} 110 | 111 | // Metable is used to include document meta in response data 112 | // e.g. {"foo": "bar"} 113 | type Metable interface { 114 | JSONAPIMeta() *Meta 115 | } 116 | 117 | // RelationshipMetable is used to include relationship meta in response data 118 | type RelationshipMetable interface { 119 | // JSONRelationshipMeta will be invoked for each relationship with the corresponding relation name (e.g. `comments`) 120 | JSONAPIRelationshipMeta(relation string) *Meta 121 | } 122 | -------------------------------------------------------------------------------- /examples/app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "net/http/httptest" 11 | "time" 12 | 13 | "github.com/google/jsonapi" 14 | ) 15 | 16 | func main() { 17 | jsonapi.Instrumentation = func(r *jsonapi.Runtime, eventType jsonapi.Event, callGUID string, dur time.Duration) { 18 | metricPrefix := r.Value("instrument").(string) 19 | 20 | if eventType == jsonapi.UnmarshalStart { 21 | fmt.Printf("%s: id, %s, started at %v\n", metricPrefix+".jsonapi_unmarshal_time", callGUID, time.Now()) 22 | } 23 | 24 | if eventType == jsonapi.UnmarshalStop { 25 | fmt.Printf("%s: id, %s, stopped at, %v , and took %v to unmarshal payload\n", metricPrefix+".jsonapi_unmarshal_time", callGUID, time.Now(), dur) 26 | } 27 | 28 | if eventType == jsonapi.MarshalStart { 29 | fmt.Printf("%s: id, %s, started at %v\n", metricPrefix+".jsonapi_marshal_time", callGUID, time.Now()) 30 | } 31 | 32 | if eventType == jsonapi.MarshalStop { 33 | fmt.Printf("%s: id, %s, stopped at, %v , and took %v to marshal payload\n", metricPrefix+".jsonapi_marshal_time", callGUID, time.Now(), dur) 34 | } 35 | } 36 | 37 | exampleHandler := &ExampleHandler{} 38 | http.HandleFunc("/blogs", exampleHandler.ServeHTTP) 39 | exerciseHandler() 40 | } 41 | 42 | func exerciseHandler() { 43 | // list 44 | req, _ := http.NewRequest(http.MethodGet, "/blogs", nil) 45 | 46 | req.Header.Set(headerAccept, jsonapi.MediaType) 47 | 48 | w := httptest.NewRecorder() 49 | 50 | fmt.Println("============ start list ===========") 51 | http.DefaultServeMux.ServeHTTP(w, req) 52 | fmt.Println("============ stop list ===========") 53 | 54 | jsonReply, _ := ioutil.ReadAll(w.Body) 55 | 56 | fmt.Println("============ jsonapi response from list ===========") 57 | fmt.Println(string(jsonReply)) 58 | fmt.Println("============== end raw jsonapi from list =============") 59 | 60 | // show 61 | req, _ = http.NewRequest(http.MethodGet, "/blogs?id=1", nil) 62 | 63 | req.Header.Set(headerAccept, jsonapi.MediaType) 64 | 65 | w = httptest.NewRecorder() 66 | 67 | fmt.Println("============ start show ===========") 68 | http.DefaultServeMux.ServeHTTP(w, req) 69 | fmt.Println("============ stop show ===========") 70 | 71 | jsonReply, _ = ioutil.ReadAll(w.Body) 72 | 73 | fmt.Println("============ jsonapi response from show ===========") 74 | fmt.Println(string(jsonReply)) 75 | fmt.Println("============== end raw jsonapi from show =============") 76 | 77 | // create 78 | blog := fixtureBlogCreate(1) 79 | in := bytes.NewBuffer(nil) 80 | jsonapi.MarshalOnePayloadEmbedded(in, blog) 81 | 82 | req, _ = http.NewRequest(http.MethodPost, "/blogs", in) 83 | 84 | req.Header.Set(headerAccept, jsonapi.MediaType) 85 | 86 | w = httptest.NewRecorder() 87 | 88 | fmt.Println("============ start create ===========") 89 | http.DefaultServeMux.ServeHTTP(w, req) 90 | fmt.Println("============ stop create ===========") 91 | 92 | buf := bytes.NewBuffer(nil) 93 | io.Copy(buf, w.Body) 94 | 95 | fmt.Println("============ jsonapi response from create ===========") 96 | fmt.Println(buf.String()) 97 | fmt.Println("============== end raw jsonapi response =============") 98 | 99 | // echo 100 | blogs := []interface{}{ 101 | fixtureBlogCreate(1), 102 | fixtureBlogCreate(2), 103 | fixtureBlogCreate(3), 104 | } 105 | in = bytes.NewBuffer(nil) 106 | jsonapi.MarshalPayload(in, blogs) 107 | 108 | req, _ = http.NewRequest(http.MethodPut, "/blogs", in) 109 | 110 | req.Header.Set(headerAccept, jsonapi.MediaType) 111 | 112 | w = httptest.NewRecorder() 113 | 114 | fmt.Println("============ start echo ===========") 115 | http.DefaultServeMux.ServeHTTP(w, req) 116 | fmt.Println("============ stop echo ===========") 117 | 118 | buf = bytes.NewBuffer(nil) 119 | io.Copy(buf, w.Body) 120 | 121 | fmt.Println("============ jsonapi response from create ===========") 122 | fmt.Println(buf.String()) 123 | fmt.Println("============== end raw jsonapi response =============") 124 | 125 | responseBlog := new(Blog) 126 | 127 | jsonapi.UnmarshalPayload(buf, responseBlog) 128 | 129 | out := bytes.NewBuffer(nil) 130 | json.NewEncoder(out).Encode(responseBlog) 131 | 132 | fmt.Println("================ Viola! Converted back our Blog struct =================") 133 | fmt.Println(string(out.Bytes())) 134 | fmt.Println("================ end marshal materialized Blog struct =================") 135 | } 136 | -------------------------------------------------------------------------------- /models_test.go: -------------------------------------------------------------------------------- 1 | package jsonapi 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | type BadModel struct { 9 | ID int `jsonapi:"primary"` 10 | } 11 | 12 | type ModelBadTypes struct { 13 | ID string `jsonapi:"primary,badtypes"` 14 | StringField string `jsonapi:"attr,string_field"` 15 | FloatField float64 `jsonapi:"attr,float_field"` 16 | TimeField time.Time `jsonapi:"attr,time_field"` 17 | TimePtrField *time.Time `jsonapi:"attr,time_ptr_field"` 18 | } 19 | 20 | type WithPointer struct { 21 | ID *uint64 `jsonapi:"primary,with-pointers"` 22 | Name *string `jsonapi:"attr,name"` 23 | IsActive *bool `jsonapi:"attr,is-active"` 24 | IntVal *int `jsonapi:"attr,int-val"` 25 | FloatVal *float32 `jsonapi:"attr,float-val"` 26 | } 27 | 28 | type TimestampModel struct { 29 | ID int `jsonapi:"primary,timestamps"` 30 | DefaultV time.Time `jsonapi:"attr,defaultv"` 31 | DefaultP *time.Time `jsonapi:"attr,defaultp"` 32 | ISO8601V time.Time `jsonapi:"attr,iso8601v,iso8601"` 33 | ISO8601P *time.Time `jsonapi:"attr,iso8601p,iso8601"` 34 | RFC3339V time.Time `jsonapi:"attr,rfc3339v,rfc3339"` 35 | RFC3339P *time.Time `jsonapi:"attr,rfc3339p,rfc3339"` 36 | } 37 | 38 | type Car struct { 39 | ID *string `jsonapi:"primary,cars"` 40 | Make *string `jsonapi:"attr,make,omitempty"` 41 | Model *string `jsonapi:"attr,model,omitempty"` 42 | Year *uint `jsonapi:"attr,year,omitempty"` 43 | } 44 | 45 | type Post struct { 46 | Blog 47 | ID uint64 `jsonapi:"primary,posts"` 48 | BlogID int `jsonapi:"attr,blog_id"` 49 | ClientID string `jsonapi:"client-id"` 50 | Title string `jsonapi:"attr,title"` 51 | Body string `jsonapi:"attr,body"` 52 | Comments []*Comment `jsonapi:"relation,comments"` 53 | LatestComment *Comment `jsonapi:"relation,latest_comment"` 54 | } 55 | 56 | type Comment struct { 57 | ID int `jsonapi:"primary,comments"` 58 | ClientID string `jsonapi:"client-id"` 59 | PostID int `jsonapi:"attr,post_id"` 60 | Body string `jsonapi:"attr,body"` 61 | } 62 | 63 | type Book struct { 64 | ID uint64 `jsonapi:"primary,books"` 65 | Author string `jsonapi:"attr,author"` 66 | ISBN string `jsonapi:"attr,isbn"` 67 | Title string `jsonapi:"attr,title,omitempty"` 68 | Description *string `jsonapi:"attr,description"` 69 | Pages *uint `jsonapi:"attr,pages,omitempty"` 70 | PublishedAt time.Time 71 | Tags []string `jsonapi:"attr,tags"` 72 | } 73 | 74 | type Blog struct { 75 | ID int `jsonapi:"primary,blogs"` 76 | ClientID string `jsonapi:"client-id"` 77 | Title string `jsonapi:"attr,title"` 78 | Posts []*Post `jsonapi:"relation,posts"` 79 | CurrentPost *Post `jsonapi:"relation,current_post"` 80 | CurrentPostID int `jsonapi:"attr,current_post_id"` 81 | CreatedAt time.Time `jsonapi:"attr,created_at"` 82 | ViewCount int `jsonapi:"attr,view_count"` 83 | } 84 | 85 | func (b *Blog) JSONAPILinks() *Links { 86 | return &Links{ 87 | "self": fmt.Sprintf("https://example.com/api/blogs/%d", b.ID), 88 | "comments": Link{ 89 | Href: fmt.Sprintf("https://example.com/api/blogs/%d/comments", b.ID), 90 | Meta: Meta{ 91 | "counts": map[string]uint{ 92 | "likes": 4, 93 | "comments": 20, 94 | }, 95 | }, 96 | }, 97 | } 98 | } 99 | 100 | func (b *Blog) JSONAPIRelationshipLinks(relation string) *Links { 101 | if relation == "posts" { 102 | return &Links{ 103 | "related": Link{ 104 | Href: fmt.Sprintf("https://example.com/api/blogs/%d/posts", b.ID), 105 | Meta: Meta{ 106 | "count": len(b.Posts), 107 | }, 108 | }, 109 | } 110 | } 111 | if relation == "current_post" { 112 | return &Links{ 113 | "self": fmt.Sprintf("https://example.com/api/posts/%s", "3"), 114 | "related": Link{ 115 | Href: fmt.Sprintf("https://example.com/api/blogs/%d/current_post", b.ID), 116 | }, 117 | } 118 | } 119 | return nil 120 | } 121 | 122 | func (b *Blog) JSONAPIMeta() *Meta { 123 | return &Meta{ 124 | "detail": "extra details regarding the blog", 125 | } 126 | } 127 | 128 | func (b *Blog) JSONAPIRelationshipMeta(relation string) *Meta { 129 | if relation == "posts" { 130 | return &Meta{ 131 | "this": map[string]interface{}{ 132 | "can": map[string]interface{}{ 133 | "go": []interface{}{ 134 | "as", 135 | "deep", 136 | map[string]interface{}{ 137 | "as": "required", 138 | }, 139 | }, 140 | }, 141 | }, 142 | } 143 | } 144 | if relation == "current_post" { 145 | return &Meta{ 146 | "detail": "extra current_post detail", 147 | } 148 | } 149 | return nil 150 | } 151 | 152 | type BadComment struct { 153 | ID uint64 `jsonapi:"primary,bad-comment"` 154 | Body string `jsonapi:"attr,body"` 155 | } 156 | 157 | func (bc *BadComment) JSONAPILinks() *Links { 158 | return &Links{ 159 | "self": []string{"invalid", "should error"}, 160 | } 161 | } 162 | 163 | type Company struct { 164 | ID string `jsonapi:"primary,companies"` 165 | Name string `jsonapi:"attr,name"` 166 | Boss Employee `jsonapi:"attr,boss"` 167 | Teams []Team `jsonapi:"attr,teams"` 168 | FoundedAt time.Time `jsonapi:"attr,founded-at,iso8601"` 169 | } 170 | 171 | type Team struct { 172 | Name string `jsonapi:"attr,name"` 173 | Leader *Employee `jsonapi:"attr,leader"` 174 | Members []Employee `jsonapi:"attr,members"` 175 | } 176 | 177 | type Employee struct { 178 | Firstname string `jsonapi:"attr,firstname"` 179 | Surname string `jsonapi:"attr,surname"` 180 | Age int `jsonapi:"attr,age"` 181 | HiredAt *time.Time `jsonapi:"attr,hired-at,iso8601"` 182 | } 183 | 184 | type CustomIntType int 185 | type CustomFloatType float64 186 | type CustomStringType string 187 | 188 | type CustomAttributeTypes struct { 189 | ID string `jsonapi:"primary,customtypes"` 190 | 191 | Int CustomIntType `jsonapi:"attr,int"` 192 | IntPtr *CustomIntType `jsonapi:"attr,intptr"` 193 | IntPtrNull *CustomIntType `jsonapi:"attr,intptrnull"` 194 | 195 | Float CustomFloatType `jsonapi:"attr,float"` 196 | String CustomStringType `jsonapi:"attr,string"` 197 | } 198 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package jsonapi 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "reflect" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | var ( 15 | // ErrBadJSONAPIStructTag is returned when the Struct field's JSON API 16 | // annotation is invalid. 17 | ErrBadJSONAPIStructTag = errors.New("Bad jsonapi struct tag format") 18 | // ErrBadJSONAPIID is returned when the Struct JSON API annotated "id" field 19 | // was not a valid numeric type. 20 | ErrBadJSONAPIID = errors.New( 21 | "id should be either string, int(8,16,32,64) or uint(8,16,32,64)") 22 | // ErrExpectedSlice is returned when a variable or argument was expected to 23 | // be a slice of *Structs; MarshalMany will return this error when its 24 | // interface{} argument is invalid. 25 | ErrExpectedSlice = errors.New("models should be a slice of struct pointers") 26 | // ErrUnexpectedType is returned when marshalling an interface; the interface 27 | // had to be a pointer or a slice; otherwise this error is returned. 28 | ErrUnexpectedType = errors.New("models should be a struct pointer or slice of struct pointers") 29 | ) 30 | 31 | // MarshalPayload writes a jsonapi response for one or many records. The 32 | // related records are sideloaded into the "included" array. If this method is 33 | // given a struct pointer as an argument it will serialize in the form 34 | // "data": {...}. If this method is given a slice of pointers, this method will 35 | // serialize in the form "data": [...] 36 | // 37 | // One Example: you could pass it, w, your http.ResponseWriter, and, models, a 38 | // ptr to a Blog to be written to the response body: 39 | // 40 | // func ShowBlog(w http.ResponseWriter, r *http.Request) { 41 | // blog := &Blog{} 42 | // 43 | // w.Header().Set("Content-Type", jsonapi.MediaType) 44 | // w.WriteHeader(http.StatusOK) 45 | // 46 | // if err := jsonapi.MarshalPayload(w, blog); err != nil { 47 | // http.Error(w, err.Error(), http.StatusInternalServerError) 48 | // } 49 | // } 50 | // 51 | // Many Example: you could pass it, w, your http.ResponseWriter, and, models, a 52 | // slice of Blog struct instance pointers to be written to the response body: 53 | // 54 | // func ListBlogs(w http.ResponseWriter, r *http.Request) { 55 | // blogs := []*Blog{} 56 | // 57 | // w.Header().Set("Content-Type", jsonapi.MediaType) 58 | // w.WriteHeader(http.StatusOK) 59 | // 60 | // if err := jsonapi.MarshalPayload(w, blogs); err != nil { 61 | // http.Error(w, err.Error(), http.StatusInternalServerError) 62 | // } 63 | // } 64 | // 65 | func MarshalPayload(w io.Writer, models interface{}) error { 66 | payload, err := Marshal(models) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | return json.NewEncoder(w).Encode(payload) 72 | } 73 | 74 | // Marshal does the same as MarshalPayload except it just returns the payload 75 | // and doesn't write out results. Useful if you use your own JSON rendering 76 | // library. 77 | func Marshal(models interface{}) (Payloader, error) { 78 | switch vals := reflect.ValueOf(models); vals.Kind() { 79 | case reflect.Slice: 80 | m, err := convertToSliceInterface(&models) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | payload, err := marshalMany(m) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | if linkableModels, isLinkable := models.(Linkable); isLinkable { 91 | jl := linkableModels.JSONAPILinks() 92 | if er := jl.validate(); er != nil { 93 | return nil, er 94 | } 95 | payload.Links = linkableModels.JSONAPILinks() 96 | } 97 | 98 | if metableModels, ok := models.(Metable); ok { 99 | payload.Meta = metableModels.JSONAPIMeta() 100 | } 101 | 102 | return payload, nil 103 | case reflect.Ptr: 104 | // Check that the pointer was to a struct 105 | if reflect.Indirect(vals).Kind() != reflect.Struct { 106 | return nil, ErrUnexpectedType 107 | } 108 | return marshalOne(models) 109 | default: 110 | return nil, ErrUnexpectedType 111 | } 112 | } 113 | 114 | // MarshalPayloadWithoutIncluded writes a jsonapi response with one or many 115 | // records, without the related records sideloaded into "included" array. 116 | // If you want to serialize the relations into the "included" array see 117 | // MarshalPayload. 118 | // 119 | // models interface{} should be either a struct pointer or a slice of struct 120 | // pointers. 121 | func MarshalPayloadWithoutIncluded(w io.Writer, model interface{}) error { 122 | payload, err := Marshal(model) 123 | if err != nil { 124 | return err 125 | } 126 | payload.clearIncluded() 127 | 128 | return json.NewEncoder(w).Encode(payload) 129 | } 130 | 131 | // marshalOne does the same as MarshalOnePayload except it just returns the 132 | // payload and doesn't write out results. Useful is you use your JSON rendering 133 | // library. 134 | func marshalOne(model interface{}) (*OnePayload, error) { 135 | included := make(map[string]*Node) 136 | 137 | rootNode, err := visitModelNode(model, &included, true) 138 | if err != nil { 139 | return nil, err 140 | } 141 | payload := &OnePayload{Data: rootNode} 142 | 143 | payload.Included = nodeMapValues(&included) 144 | 145 | return payload, nil 146 | } 147 | 148 | // marshalMany does the same as MarshalManyPayload except it just returns the 149 | // payload and doesn't write out results. Useful is you use your JSON rendering 150 | // library. 151 | func marshalMany(models []interface{}) (*ManyPayload, error) { 152 | payload := &ManyPayload{ 153 | Data: []*Node{}, 154 | } 155 | included := map[string]*Node{} 156 | 157 | for _, model := range models { 158 | node, err := visitModelNode(model, &included, true) 159 | if err != nil { 160 | return nil, err 161 | } 162 | payload.Data = append(payload.Data, node) 163 | } 164 | payload.Included = nodeMapValues(&included) 165 | 166 | return payload, nil 167 | } 168 | 169 | // MarshalOnePayloadEmbedded - This method not meant to for use in 170 | // implementation code, although feel free. The purpose of this 171 | // method is for use in tests. In most cases, your request 172 | // payloads for create will be embedded rather than sideloaded for 173 | // related records. This method will serialize a single struct 174 | // pointer into an embedded json response. In other words, there 175 | // will be no, "included", array in the json all relationships will 176 | // be serailized inline in the data. 177 | // 178 | // However, in tests, you may want to construct payloads to post 179 | // to create methods that are embedded to most closely resemble 180 | // the payloads that will be produced by the client. This is what 181 | // this method is intended for. 182 | // 183 | // model interface{} should be a pointer to a struct. 184 | func MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error { 185 | rootNode, err := visitModelNode(model, nil, false) 186 | if err != nil { 187 | return err 188 | } 189 | 190 | payload := &OnePayload{Data: rootNode} 191 | 192 | return json.NewEncoder(w).Encode(payload) 193 | } 194 | 195 | func visitModelNode(model interface{}, included *map[string]*Node, 196 | sideload bool) (*Node, error) { 197 | node := new(Node) 198 | 199 | var er error 200 | value := reflect.ValueOf(model) 201 | if value.IsNil() { 202 | return nil, nil 203 | } 204 | 205 | modelValue := value.Elem() 206 | modelType := value.Type().Elem() 207 | 208 | for i := 0; i < modelValue.NumField(); i++ { 209 | structField := modelValue.Type().Field(i) 210 | tag := structField.Tag.Get(annotationJSONAPI) 211 | if tag == "" { 212 | continue 213 | } 214 | 215 | fieldValue := modelValue.Field(i) 216 | fieldType := modelType.Field(i) 217 | 218 | args := strings.Split(tag, annotationSeperator) 219 | 220 | if len(args) < 1 { 221 | er = ErrBadJSONAPIStructTag 222 | break 223 | } 224 | 225 | annotation := args[0] 226 | 227 | if (annotation == annotationClientID && len(args) != 1) || 228 | (annotation != annotationClientID && len(args) < 2) { 229 | er = ErrBadJSONAPIStructTag 230 | break 231 | } 232 | 233 | if annotation == annotationPrimary { 234 | v := fieldValue 235 | 236 | // Deal with PTRS 237 | var kind reflect.Kind 238 | if fieldValue.Kind() == reflect.Ptr { 239 | kind = fieldType.Type.Elem().Kind() 240 | v = reflect.Indirect(fieldValue) 241 | } else { 242 | kind = fieldType.Type.Kind() 243 | } 244 | 245 | // Handle allowed types 246 | switch kind { 247 | case reflect.String: 248 | node.ID = v.Interface().(string) 249 | case reflect.Int: 250 | node.ID = strconv.FormatInt(int64(v.Interface().(int)), 10) 251 | case reflect.Int8: 252 | node.ID = strconv.FormatInt(int64(v.Interface().(int8)), 10) 253 | case reflect.Int16: 254 | node.ID = strconv.FormatInt(int64(v.Interface().(int16)), 10) 255 | case reflect.Int32: 256 | node.ID = strconv.FormatInt(int64(v.Interface().(int32)), 10) 257 | case reflect.Int64: 258 | node.ID = strconv.FormatInt(v.Interface().(int64), 10) 259 | case reflect.Uint: 260 | node.ID = strconv.FormatUint(uint64(v.Interface().(uint)), 10) 261 | case reflect.Uint8: 262 | node.ID = strconv.FormatUint(uint64(v.Interface().(uint8)), 10) 263 | case reflect.Uint16: 264 | node.ID = strconv.FormatUint(uint64(v.Interface().(uint16)), 10) 265 | case reflect.Uint32: 266 | node.ID = strconv.FormatUint(uint64(v.Interface().(uint32)), 10) 267 | case reflect.Uint64: 268 | node.ID = strconv.FormatUint(v.Interface().(uint64), 10) 269 | default: 270 | // We had a JSON float (numeric), but our field was not one of the 271 | // allowed numeric types 272 | er = ErrBadJSONAPIID 273 | } 274 | 275 | if er != nil { 276 | break 277 | } 278 | 279 | node.Type = args[1] 280 | } else if annotation == annotationClientID { 281 | clientID := fieldValue.String() 282 | if clientID != "" { 283 | node.ClientID = clientID 284 | } 285 | } else if annotation == annotationAttribute { 286 | var omitEmpty, iso8601, rfc3339 bool 287 | 288 | if len(args) > 2 { 289 | for _, arg := range args[2:] { 290 | switch arg { 291 | case annotationOmitEmpty: 292 | omitEmpty = true 293 | case annotationISO8601: 294 | iso8601 = true 295 | case annotationRFC3339: 296 | rfc3339 = true 297 | } 298 | } 299 | } 300 | 301 | if node.Attributes == nil { 302 | node.Attributes = make(map[string]interface{}) 303 | } 304 | 305 | if fieldValue.Type() == reflect.TypeOf(time.Time{}) { 306 | t := fieldValue.Interface().(time.Time) 307 | 308 | if t.IsZero() { 309 | continue 310 | } 311 | 312 | if iso8601 { 313 | node.Attributes[args[1]] = t.UTC().Format(iso8601TimeFormat) 314 | } else if rfc3339 { 315 | node.Attributes[args[1]] = t.UTC().Format(time.RFC3339) 316 | } else { 317 | node.Attributes[args[1]] = t.Unix() 318 | } 319 | } else if fieldValue.Type() == reflect.TypeOf(new(time.Time)) { 320 | // A time pointer may be nil 321 | if fieldValue.IsNil() { 322 | if omitEmpty { 323 | continue 324 | } 325 | 326 | node.Attributes[args[1]] = nil 327 | } else { 328 | tm := fieldValue.Interface().(*time.Time) 329 | 330 | if tm.IsZero() && omitEmpty { 331 | continue 332 | } 333 | 334 | if iso8601 { 335 | node.Attributes[args[1]] = tm.UTC().Format(iso8601TimeFormat) 336 | } else if rfc3339 { 337 | node.Attributes[args[1]] = tm.UTC().Format(time.RFC3339) 338 | } else { 339 | node.Attributes[args[1]] = tm.Unix() 340 | } 341 | } 342 | } else { 343 | // Dealing with a fieldValue that is not a time 344 | emptyValue := reflect.Zero(fieldValue.Type()) 345 | 346 | // See if we need to omit this field 347 | if omitEmpty && reflect.DeepEqual(fieldValue.Interface(), emptyValue.Interface()) { 348 | continue 349 | } 350 | 351 | strAttr, ok := fieldValue.Interface().(string) 352 | if ok { 353 | node.Attributes[args[1]] = strAttr 354 | } else { 355 | node.Attributes[args[1]] = fieldValue.Interface() 356 | } 357 | } 358 | } else if annotation == annotationRelation { 359 | var omitEmpty bool 360 | 361 | //add support for 'omitempty' struct tag for marshaling as absent 362 | if len(args) > 2 { 363 | omitEmpty = args[2] == annotationOmitEmpty 364 | } 365 | 366 | isSlice := fieldValue.Type().Kind() == reflect.Slice 367 | if omitEmpty && 368 | (isSlice && fieldValue.Len() < 1 || 369 | (!isSlice && fieldValue.IsNil())) { 370 | continue 371 | } 372 | 373 | if node.Relationships == nil { 374 | node.Relationships = make(map[string]interface{}) 375 | } 376 | 377 | var relLinks *Links 378 | if linkableModel, ok := model.(RelationshipLinkable); ok { 379 | relLinks = linkableModel.JSONAPIRelationshipLinks(args[1]) 380 | } 381 | 382 | var relMeta *Meta 383 | if metableModel, ok := model.(RelationshipMetable); ok { 384 | relMeta = metableModel.JSONAPIRelationshipMeta(args[1]) 385 | } 386 | 387 | if isSlice { 388 | // to-many relationship 389 | relationship, err := visitModelNodeRelationships( 390 | fieldValue, 391 | included, 392 | sideload, 393 | ) 394 | if err != nil { 395 | er = err 396 | break 397 | } 398 | relationship.Links = relLinks 399 | relationship.Meta = relMeta 400 | 401 | if sideload { 402 | shallowNodes := []*Node{} 403 | for _, n := range relationship.Data { 404 | appendIncluded(included, n) 405 | shallowNodes = append(shallowNodes, toShallowNode(n)) 406 | } 407 | 408 | node.Relationships[args[1]] = &RelationshipManyNode{ 409 | Data: shallowNodes, 410 | Links: relationship.Links, 411 | Meta: relationship.Meta, 412 | } 413 | } else { 414 | node.Relationships[args[1]] = relationship 415 | } 416 | } else { 417 | // to-one relationships 418 | 419 | // Handle null relationship case 420 | if fieldValue.IsNil() { 421 | node.Relationships[args[1]] = &RelationshipOneNode{Data: nil} 422 | continue 423 | } 424 | 425 | relationship, err := visitModelNode( 426 | fieldValue.Interface(), 427 | included, 428 | sideload, 429 | ) 430 | if err != nil { 431 | er = err 432 | break 433 | } 434 | 435 | if sideload { 436 | appendIncluded(included, relationship) 437 | node.Relationships[args[1]] = &RelationshipOneNode{ 438 | Data: toShallowNode(relationship), 439 | Links: relLinks, 440 | Meta: relMeta, 441 | } 442 | } else { 443 | node.Relationships[args[1]] = &RelationshipOneNode{ 444 | Data: relationship, 445 | Links: relLinks, 446 | Meta: relMeta, 447 | } 448 | } 449 | } 450 | 451 | } else { 452 | er = ErrBadJSONAPIStructTag 453 | break 454 | } 455 | } 456 | 457 | if er != nil { 458 | return nil, er 459 | } 460 | 461 | if linkableModel, isLinkable := model.(Linkable); isLinkable { 462 | jl := linkableModel.JSONAPILinks() 463 | if er := jl.validate(); er != nil { 464 | return nil, er 465 | } 466 | node.Links = linkableModel.JSONAPILinks() 467 | } 468 | 469 | if metableModel, ok := model.(Metable); ok { 470 | node.Meta = metableModel.JSONAPIMeta() 471 | } 472 | 473 | return node, nil 474 | } 475 | 476 | func toShallowNode(node *Node) *Node { 477 | return &Node{ 478 | ID: node.ID, 479 | Type: node.Type, 480 | } 481 | } 482 | 483 | func visitModelNodeRelationships(models reflect.Value, included *map[string]*Node, 484 | sideload bool) (*RelationshipManyNode, error) { 485 | nodes := []*Node{} 486 | 487 | for i := 0; i < models.Len(); i++ { 488 | n := models.Index(i).Interface() 489 | 490 | node, err := visitModelNode(n, included, sideload) 491 | if err != nil { 492 | return nil, err 493 | } 494 | 495 | nodes = append(nodes, node) 496 | } 497 | 498 | return &RelationshipManyNode{Data: nodes}, nil 499 | } 500 | 501 | func appendIncluded(m *map[string]*Node, nodes ...*Node) { 502 | included := *m 503 | 504 | for _, n := range nodes { 505 | k := fmt.Sprintf("%s,%s", n.Type, n.ID) 506 | 507 | if _, hasNode := included[k]; hasNode { 508 | continue 509 | } 510 | 511 | included[k] = n 512 | } 513 | } 514 | 515 | func nodeMapValues(m *map[string]*Node) []*Node { 516 | mp := *m 517 | nodes := make([]*Node, len(mp)) 518 | 519 | i := 0 520 | for _, n := range mp { 521 | nodes[i] = n 522 | i++ 523 | } 524 | 525 | return nodes 526 | } 527 | 528 | func convertToSliceInterface(i *interface{}) ([]interface{}, error) { 529 | vals := reflect.ValueOf(*i) 530 | if vals.Kind() != reflect.Slice { 531 | return nil, ErrExpectedSlice 532 | } 533 | var response []interface{} 534 | for x := 0; x < vals.Len(); x++ { 535 | response = append(response, vals.Index(x).Interface()) 536 | } 537 | return response, nil 538 | } 539 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsonapi 2 | 3 | [![Build Status](https://travis-ci.org/google/jsonapi.svg?branch=master)](https://travis-ci.org/google/jsonapi) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/google/jsonapi)](https://goreportcard.com/report/github.com/google/jsonapi) 5 | [![GoDoc](https://godoc.org/github.com/google/jsonapi?status.svg)](http://godoc.org/github.com/google/jsonapi) 6 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 7 | 8 | A serializer/deserializer for JSON payloads that comply to the 9 | [JSON API - jsonapi.org](http://jsonapi.org) spec in go. 10 | 11 | 12 | 13 | ## Installation 14 | 15 | ``` 16 | go get -u github.com/google/jsonapi 17 | ``` 18 | 19 | Or, see [Alternative Installation](#alternative-installation). 20 | 21 | ## Background 22 | 23 | You are working in your Go web application and you have a struct that is 24 | organized similarly to your database schema. You need to send and 25 | receive json payloads that adhere to the JSON API spec. Once you realize that 26 | your json needed to take on this special form, you go down the path of 27 | creating more structs to be able to serialize and deserialize JSON API 28 | payloads. Then there are more models required with this additional 29 | structure. Ugh! With JSON API, you can keep your model structs as is and 30 | use [StructTags](http://golang.org/pkg/reflect/#StructTag) to indicate 31 | to JSON API how you want your response built or your request 32 | deserialized. What about your relationships? JSON API supports 33 | relationships out of the box and will even put them in your response 34 | into an `included` side-loaded slice--that contains associated records. 35 | 36 | ## Introduction 37 | 38 | JSON API uses [StructField](http://golang.org/pkg/reflect/#StructField) 39 | tags to annotate the structs fields that you already have and use in 40 | your app and then reads and writes [JSON API](http://jsonapi.org) 41 | output based on the instructions you give the library in your JSON API 42 | tags. Let's take an example. In your app, you most likely have structs 43 | that look similar to these: 44 | 45 | 46 | ```go 47 | type Blog struct { 48 | ID int `json:"id"` 49 | Title string `json:"title"` 50 | Posts []*Post `json:"posts"` 51 | CurrentPost *Post `json:"current_post"` 52 | CurrentPostId int `json:"current_post_id"` 53 | CreatedAt time.Time `json:"created_at"` 54 | ViewCount int `json:"view_count"` 55 | } 56 | 57 | type Post struct { 58 | ID int `json:"id"` 59 | BlogID int `json:"blog_id"` 60 | Title string `json:"title"` 61 | Body string `json:"body"` 62 | Comments []*Comment `json:"comments"` 63 | } 64 | 65 | type Comment struct { 66 | Id int `json:"id"` 67 | PostID int `json:"post_id"` 68 | Body string `json:"body"` 69 | Likes uint `json:"likes_count,omitempty"` 70 | } 71 | ``` 72 | 73 | These structs may or may not resemble the layout of your database. But 74 | these are the ones that you want to use right? You wouldn't want to use 75 | structs like those that JSON API sends because it is difficult to get at 76 | all of your data easily. 77 | 78 | ## Example App 79 | 80 | [examples/app.go](https://github.com/google/jsonapi/blob/master/examples/app.go) 81 | 82 | This program demonstrates the implementation of a create, a show, 83 | and a list [http.Handler](http://golang.org/pkg/net/http#Handler). It 84 | outputs some example requests and responses as well as serialized 85 | examples of the source/target structs to json. That is to say, I show 86 | you that the library has successfully taken your JSON API request and 87 | turned it into your struct types. 88 | 89 | To run, 90 | 91 | * Make sure you have [Go installed](https://golang.org/doc/install) 92 | * Create the following directories or similar: `~/go` 93 | * Set `GOPATH` to `PWD` in your shell session, `export GOPATH=$PWD` 94 | * `go get github.com/google/jsonapi`. (Append `-u` after `get` if you 95 | are updating.) 96 | * `cd $GOPATH/src/github.com/google/jsonapi/examples` 97 | * `go build && ./examples` 98 | 99 | ## `jsonapi` Tag Reference 100 | 101 | ### Example 102 | 103 | The `jsonapi` [StructTags](http://golang.org/pkg/reflect/#StructTag) 104 | tells this library how to marshal and unmarshal your structs into 105 | JSON API payloads and your JSON API payloads to structs, respectively. 106 | Then Use JSON API's Marshal and Unmarshal methods to construct and read 107 | your responses and replies. Here's an example of the structs above 108 | using JSON API tags: 109 | 110 | ```go 111 | type Blog struct { 112 | ID int `jsonapi:"primary,blogs"` 113 | Title string `jsonapi:"attr,title"` 114 | Posts []*Post `jsonapi:"relation,posts"` 115 | CurrentPost *Post `jsonapi:"relation,current_post"` 116 | CurrentPostID int `jsonapi:"attr,current_post_id"` 117 | CreatedAt time.Time `jsonapi:"attr,created_at"` 118 | ViewCount int `jsonapi:"attr,view_count"` 119 | } 120 | 121 | type Post struct { 122 | ID int `jsonapi:"primary,posts"` 123 | BlogID int `jsonapi:"attr,blog_id"` 124 | Title string `jsonapi:"attr,title"` 125 | Body string `jsonapi:"attr,body"` 126 | Comments []*Comment `jsonapi:"relation,comments"` 127 | } 128 | 129 | type Comment struct { 130 | ID int `jsonapi:"primary,comments"` 131 | PostID int `jsonapi:"attr,post_id"` 132 | Body string `jsonapi:"attr,body"` 133 | Likes uint `jsonapi:"attr,likes-count,omitempty"` 134 | } 135 | ``` 136 | 137 | ### Permitted Tag Values 138 | 139 | #### `primary` 140 | 141 | ``` 142 | `jsonapi:"primary,"` 143 | ``` 144 | 145 | This indicates this is the primary key field for this struct type. 146 | Tag value arguments are comma separated. The first argument must be, 147 | `primary`, and the second must be the name that should appear in the 148 | `type`\* field for all data objects that represent this type of model. 149 | 150 | \* According the [JSON API](http://jsonapi.org) spec, the plural record 151 | types are shown in the examples, but not required. 152 | 153 | #### `attr` 154 | 155 | ``` 156 | `jsonapi:"attr,,"` 157 | ``` 158 | 159 | These fields' values will end up in the `attributes`hash for a record. 160 | The first argument must be, `attr`, and the second should be the name 161 | for the key to display in the `attributes` hash for that record. The optional 162 | third argument is `omitempty` - if it is present the field will not be present 163 | in the `"attributes"` if the field's value is equivalent to the field types 164 | empty value (ie if the `count` field is of type `int`, `omitempty` will omit the 165 | field when `count` has a value of `0`). Lastly, the spec indicates that 166 | `attributes` key names should be dasherized for multiple word field names. 167 | 168 | #### `relation` 169 | 170 | ``` 171 | `jsonapi:"relation,,"` 172 | ``` 173 | 174 | Relations are struct fields that represent a one-to-one or one-to-many 175 | relationship with other structs. JSON API will traverse the graph of 176 | relationships and marshal or unmarshal records. The first argument must 177 | be, `relation`, and the second should be the name of the relationship, 178 | used as the key in the `relationships` hash for the record. The optional 179 | third argument is `omitempty` - if present will prevent non existent to-one and 180 | to-many from being serialized. 181 | 182 | ## Methods Reference 183 | 184 | **All `Marshal` and `Unmarshal` methods expect pointers to struct 185 | instance or slices of the same contained with the `interface{}`s** 186 | 187 | Now you have your structs prepared to be serialized or materialized, What 188 | about the rest? 189 | 190 | ### Create Record Example 191 | 192 | You can Unmarshal a JSON API payload using 193 | [jsonapi.UnmarshalPayload](http://godoc.org/github.com/google/jsonapi#UnmarshalPayload). 194 | It reads from an [io.Reader](https://golang.org/pkg/io/#Reader) 195 | containing a JSON API payload for one record (but can have related 196 | records). Then, it materializes a struct that you created and passed in 197 | (using new or &). Again, the method supports single records only, at 198 | the top level, in request payloads at the moment. Bulk creates and 199 | updates are not supported yet. 200 | 201 | After saving your record, you can use, 202 | [MarshalOnePayload](http://godoc.org/github.com/google/jsonapi#MarshalOnePayload), 203 | to write the JSON API response to an 204 | [io.Writer](https://golang.org/pkg/io/#Writer). 205 | 206 | #### `UnmarshalPayload` 207 | 208 | ```go 209 | UnmarshalPayload(in io.Reader, model interface{}) 210 | ``` 211 | 212 | Visit [godoc](http://godoc.org/github.com/google/jsonapi#UnmarshalPayload) 213 | 214 | #### `MarshalPayload` 215 | 216 | ```go 217 | MarshalPayload(w io.Writer, models interface{}) error 218 | ``` 219 | 220 | Visit [godoc](http://godoc.org/github.com/google/jsonapi#MarshalPayload) 221 | 222 | Writes a JSON API response, with related records sideloaded, into an 223 | `included` array. This method encodes a response for either a single record or 224 | many records. 225 | 226 | ##### Handler Example Code 227 | 228 | ```go 229 | func CreateBlog(w http.ResponseWriter, r *http.Request) { 230 | blog := new(Blog) 231 | 232 | if err := jsonapi.UnmarshalPayload(r.Body, blog); err != nil { 233 | http.Error(w, err.Error(), http.StatusInternalServerError) 234 | return 235 | } 236 | 237 | // ...save your blog... 238 | 239 | w.Header().Set("Content-Type", jsonapi.MediaType) 240 | w.WriteHeader(http.StatusCreated) 241 | 242 | if err := jsonapi.MarshalPayload(w, blog); err != nil { 243 | http.Error(w, err.Error(), http.StatusInternalServerError) 244 | } 245 | } 246 | ``` 247 | 248 | ### Create Records Example 249 | 250 | #### `UnmarshalManyPayload` 251 | 252 | ```go 253 | UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) 254 | ``` 255 | 256 | Visit [godoc](http://godoc.org/github.com/google/jsonapi#UnmarshalManyPayload) 257 | 258 | Takes an `io.Reader` and a `reflect.Type` representing the uniform type 259 | contained within the `"data"` JSON API member. 260 | 261 | ##### Handler Example Code 262 | 263 | ```go 264 | func CreateBlogs(w http.ResponseWriter, r *http.Request) { 265 | // ...create many blogs at once 266 | 267 | blogs, err := UnmarshalManyPayload(r.Body, reflect.TypeOf(new(Blog))) 268 | if err != nil { 269 | t.Fatal(err) 270 | } 271 | 272 | for _, blog := range blogs { 273 | b, ok := blog.(*Blog) 274 | // ...save each of your blogs 275 | } 276 | 277 | w.Header().Set("Content-Type", jsonapi.MediaType) 278 | w.WriteHeader(http.StatusCreated) 279 | 280 | if err := jsonapi.MarshalPayload(w, blogs); err != nil { 281 | http.Error(w, err.Error(), http.StatusInternalServerError) 282 | } 283 | } 284 | ``` 285 | 286 | 287 | ### Links 288 | 289 | If you need to include [link objects](http://jsonapi.org/format/#document-links) along with response data, implement the `Linkable` interface for document-links, and `RelationshipLinkable` for relationship links: 290 | 291 | ```go 292 | func (post Post) JSONAPILinks() *Links { 293 | return &Links{ 294 | "self": "href": fmt.Sprintf("https://example.com/posts/%d", post.ID), 295 | "comments": Link{ 296 | Href: fmt.Sprintf("https://example.com/api/blogs/%d/comments", post.ID), 297 | Meta: map[string]interface{}{ 298 | "counts": map[string]uint{ 299 | "likes": 4, 300 | }, 301 | }, 302 | }, 303 | } 304 | } 305 | 306 | // Invoked for each relationship defined on the Post struct when marshaled 307 | func (post Post) JSONAPIRelationshipLinks(relation string) *Links { 308 | if relation == "comments" { 309 | return &Links{ 310 | "related": fmt.Sprintf("https://example.com/posts/%d/comments", post.ID), 311 | } 312 | } 313 | return nil 314 | } 315 | ``` 316 | 317 | ### Meta 318 | 319 | If you need to include [meta objects](http://jsonapi.org/format/#document-meta) along with response data, implement the `Metable` interface for document-meta, and `RelationshipMetable` for relationship meta: 320 | 321 | ```go 322 | func (post Post) JSONAPIMeta() *Meta { 323 | return &Meta{ 324 | "details": "sample details here", 325 | } 326 | } 327 | 328 | // Invoked for each relationship defined on the Post struct when marshaled 329 | func (post Post) JSONAPIRelationshipMeta(relation string) *Meta { 330 | if relation == "comments" { 331 | return &Meta{ 332 | "this": map[string]interface{}{ 333 | "can": map[string]interface{}{ 334 | "go": []interface{}{ 335 | "as", 336 | "deep", 337 | map[string]interface{}{ 338 | "as": "required", 339 | }, 340 | }, 341 | }, 342 | }, 343 | } 344 | } 345 | return nil 346 | } 347 | ``` 348 | 349 | ### Custom types 350 | 351 | Custom types are supported for primitive types, only, as attributes. Examples, 352 | 353 | ```go 354 | type CustomIntType int 355 | type CustomFloatType float64 356 | type CustomStringType string 357 | ``` 358 | 359 | Types like following are not supported, but may be in the future: 360 | 361 | ```go 362 | type CustomMapType map[string]interface{} 363 | type CustomSliceMapType []map[string]interface{} 364 | ``` 365 | 366 | ### Errors 367 | This package also implements support for JSON API compatible `errors` payloads using the following types. 368 | 369 | #### `MarshalErrors` 370 | ```go 371 | MarshalErrors(w io.Writer, errs []*ErrorObject) error 372 | ``` 373 | 374 | Writes a JSON API response using the given `[]error`. 375 | 376 | #### `ErrorsPayload` 377 | ```go 378 | type ErrorsPayload struct { 379 | Errors []*ErrorObject `json:"errors"` 380 | } 381 | ``` 382 | 383 | ErrorsPayload is a serializer struct for representing a valid JSON API errors payload. 384 | 385 | #### `ErrorObject` 386 | ```go 387 | type ErrorObject struct { ... } 388 | 389 | // Error implements the `Error` interface. 390 | func (e *ErrorObject) Error() string { 391 | return fmt.Sprintf("Error: %s %s\n", e.Title, e.Detail) 392 | } 393 | ``` 394 | 395 | ErrorObject is an `Error` implementation as well as an implementation of the JSON API error object. 396 | 397 | 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. 398 | 399 | ##### Errors Example Code 400 | ```go 401 | // An error has come up in your code, so set an appropriate status, and serialize the error. 402 | if err := validate(&myStructToValidate); err != nil { 403 | context.SetStatusCode(http.StatusBadRequest) // Or however you need to set a status. 404 | jsonapi.MarshalErrors(w, []*ErrorObject{{ 405 | Title: "Validation Error", 406 | Detail: "Given request body was invalid.", 407 | Status: "400", 408 | Meta: map[string]interface{}{"field": "some_field", "error": "bad type", "expected": "string", "received": "float64"}, 409 | }}) 410 | return 411 | } 412 | ``` 413 | 414 | ## Testing 415 | 416 | ### `MarshalOnePayloadEmbedded` 417 | 418 | ```go 419 | MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error 420 | ``` 421 | 422 | Visit [godoc](http://godoc.org/github.com/google/jsonapi#MarshalOnePayloadEmbedded) 423 | 424 | This method is not strictly meant to for use in implementation code, 425 | although feel free. It was mainly created for use in tests; in most cases, 426 | your request payloads for create will be embedded rather than sideloaded 427 | for related records. This method will serialize a single struct pointer 428 | into an embedded json response. In other words, there will be no, 429 | `included`, array in the json; all relationships will be serialized 430 | inline with the data. 431 | 432 | However, in tests, you may want to construct payloads to post to create 433 | methods that are embedded to most closely model the payloads that will 434 | be produced by the client. This method aims to enable that. 435 | 436 | ### Example 437 | 438 | ```go 439 | out := bytes.NewBuffer(nil) 440 | 441 | // testModel returns a pointer to a Blog 442 | jsonapi.MarshalOnePayloadEmbedded(out, testModel()) 443 | 444 | h := new(BlogsHandler) 445 | 446 | w := httptest.NewRecorder() 447 | r, _ := http.NewRequest(http.MethodPost, "/blogs", out) 448 | 449 | h.CreateBlog(w, r) 450 | 451 | blog := new(Blog) 452 | jsonapi.UnmarshalPayload(w.Body, blog) 453 | 454 | // ... assert stuff about blog here ... 455 | ``` 456 | 457 | ## Alternative Installation 458 | I use git subtrees to manage dependencies rather than `go get` so that 459 | the src is committed to my repo. 460 | 461 | ``` 462 | git subtree add --squash --prefix=src/github.com/google/jsonapi https://github.com/google/jsonapi.git master 463 | ``` 464 | 465 | To update, 466 | 467 | ``` 468 | git subtree pull --squash --prefix=src/github.com/google/jsonapi https://github.com/google/jsonapi.git master 469 | ``` 470 | 471 | This assumes that I have my repo structured with a `src` dir containing 472 | a collection of packages and `GOPATH` is set to the root 473 | folder--containing `src`. 474 | 475 | ## Contributing 476 | 477 | Fork, Change, Pull Request *with tests*. 478 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package jsonapi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "reflect" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | const ( 16 | unsupportedStructTagMsg = "Unsupported jsonapi tag annotation, %s" 17 | ) 18 | 19 | var ( 20 | // ErrInvalidTime is returned when a struct has a time.Time type field, but 21 | // the JSON value was not a unix timestamp integer. 22 | ErrInvalidTime = errors.New("Only numbers can be parsed as dates, unix timestamps") 23 | // ErrInvalidISO8601 is returned when a struct has a time.Time type field and includes 24 | // "iso8601" in the tag spec, but the JSON value was not an ISO8601 timestamp string. 25 | ErrInvalidISO8601 = errors.New("Only strings can be parsed as dates, ISO8601 timestamps") 26 | // ErrInvalidRFC3339 is returned when a struct has a time.Time type field and includes 27 | // "rfc3339" in the tag spec, but the JSON value was not an RFC3339 timestamp string. 28 | ErrInvalidRFC3339 = errors.New("Only strings can be parsed as dates, RFC3339 timestamps") 29 | // ErrUnknownFieldNumberType is returned when the JSON value was a float 30 | // (numeric) but the Struct field was a non numeric type (i.e. not int, uint, 31 | // float, etc) 32 | ErrUnknownFieldNumberType = errors.New("The struct field was not of a known number type") 33 | // ErrInvalidType is returned when the given type is incompatible with the expected type. 34 | ErrInvalidType = errors.New("Invalid type provided") // I wish we used punctuation. 35 | 36 | ) 37 | 38 | // ErrUnsupportedPtrType is returned when the Struct field was a pointer but 39 | // the JSON value was of a different type 40 | type ErrUnsupportedPtrType struct { 41 | rf reflect.Value 42 | t reflect.Type 43 | structField reflect.StructField 44 | } 45 | 46 | func (eupt ErrUnsupportedPtrType) Error() string { 47 | typeName := eupt.t.Elem().Name() 48 | kind := eupt.t.Elem().Kind() 49 | if kind.String() != "" && kind.String() != typeName { 50 | typeName = fmt.Sprintf("%s (%s)", typeName, kind.String()) 51 | } 52 | return fmt.Sprintf( 53 | "jsonapi: Can't unmarshal %+v (%s) to struct field `%s`, which is a pointer to `%s`", 54 | eupt.rf, eupt.rf.Type().Kind(), eupt.structField.Name, typeName, 55 | ) 56 | } 57 | 58 | func newErrUnsupportedPtrType(rf reflect.Value, t reflect.Type, structField reflect.StructField) error { 59 | return ErrUnsupportedPtrType{rf, t, structField} 60 | } 61 | 62 | // UnmarshalPayload converts an io into a struct instance using jsonapi tags on 63 | // struct fields. This method supports single request payloads only, at the 64 | // moment. Bulk creates and updates are not supported yet. 65 | // 66 | // Will Unmarshal embedded and sideloaded payloads. The latter is only possible if the 67 | // object graph is complete. That is, in the "relationships" data there are type and id, 68 | // keys that correspond to records in the "included" array. 69 | // 70 | // For example you could pass it, in, req.Body and, model, a BlogPost 71 | // struct instance to populate in an http handler, 72 | // 73 | // func CreateBlog(w http.ResponseWriter, r *http.Request) { 74 | // blog := new(Blog) 75 | // 76 | // if err := jsonapi.UnmarshalPayload(r.Body, blog); err != nil { 77 | // http.Error(w, err.Error(), 500) 78 | // return 79 | // } 80 | // 81 | // // ...do stuff with your blog... 82 | // 83 | // w.Header().Set("Content-Type", jsonapi.MediaType) 84 | // w.WriteHeader(201) 85 | // 86 | // if err := jsonapi.MarshalPayload(w, blog); err != nil { 87 | // http.Error(w, err.Error(), 500) 88 | // } 89 | // } 90 | // 91 | // 92 | // Visit https://github.com/google/jsonapi#create for more info. 93 | // 94 | // model interface{} should be a pointer to a struct. 95 | func UnmarshalPayload(in io.Reader, model interface{}) error { 96 | payload := new(OnePayload) 97 | 98 | if err := json.NewDecoder(in).Decode(payload); err != nil { 99 | return err 100 | } 101 | 102 | if payload.Included != nil { 103 | includedMap := make(map[string]*Node) 104 | for _, included := range payload.Included { 105 | key := fmt.Sprintf("%s,%s", included.Type, included.ID) 106 | includedMap[key] = included 107 | } 108 | 109 | return unmarshalNode(payload.Data, reflect.ValueOf(model), &includedMap) 110 | } 111 | return unmarshalNode(payload.Data, reflect.ValueOf(model), nil) 112 | } 113 | 114 | // UnmarshalManyPayload converts an io into a set of struct instances using 115 | // jsonapi tags on the type's struct fields. 116 | func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) { 117 | payload := new(ManyPayload) 118 | 119 | if err := json.NewDecoder(in).Decode(payload); err != nil { 120 | return nil, err 121 | } 122 | 123 | models := []interface{}{} // will be populated from the "data" 124 | includedMap := map[string]*Node{} // will be populate from the "included" 125 | 126 | if payload.Included != nil { 127 | for _, included := range payload.Included { 128 | key := fmt.Sprintf("%s,%s", included.Type, included.ID) 129 | includedMap[key] = included 130 | } 131 | } 132 | 133 | for _, data := range payload.Data { 134 | model := reflect.New(t.Elem()) 135 | err := unmarshalNode(data, model, &includedMap) 136 | if err != nil { 137 | return nil, err 138 | } 139 | models = append(models, model.Interface()) 140 | } 141 | 142 | return models, nil 143 | } 144 | 145 | func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) { 146 | defer func() { 147 | if r := recover(); r != nil { 148 | err = fmt.Errorf("data is not a jsonapi representation of '%v'", model.Type()) 149 | } 150 | }() 151 | 152 | modelValue := model.Elem() 153 | modelType := modelValue.Type() 154 | 155 | var er error 156 | 157 | for i := 0; i < modelValue.NumField(); i++ { 158 | fieldType := modelType.Field(i) 159 | tag := fieldType.Tag.Get("jsonapi") 160 | if tag == "" { 161 | continue 162 | } 163 | 164 | fieldValue := modelValue.Field(i) 165 | 166 | args := strings.Split(tag, ",") 167 | if len(args) < 1 { 168 | er = ErrBadJSONAPIStructTag 169 | break 170 | } 171 | 172 | annotation := args[0] 173 | 174 | if (annotation == annotationClientID && len(args) != 1) || 175 | (annotation != annotationClientID && len(args) < 2) { 176 | er = ErrBadJSONAPIStructTag 177 | break 178 | } 179 | 180 | if annotation == annotationPrimary { 181 | // Check the JSON API Type 182 | if data.Type != args[1] { 183 | er = fmt.Errorf( 184 | "Trying to Unmarshal an object of type %#v, but %#v does not match", 185 | data.Type, 186 | args[1], 187 | ) 188 | break 189 | } 190 | 191 | if data.ID == "" { 192 | continue 193 | } 194 | 195 | // ID will have to be transmitted as astring per the JSON API spec 196 | v := reflect.ValueOf(data.ID) 197 | 198 | // Deal with PTRS 199 | var kind reflect.Kind 200 | if fieldValue.Kind() == reflect.Ptr { 201 | kind = fieldType.Type.Elem().Kind() 202 | } else { 203 | kind = fieldType.Type.Kind() 204 | } 205 | 206 | // Handle String case 207 | if kind == reflect.String { 208 | assign(fieldValue, v) 209 | continue 210 | } 211 | 212 | // Value was not a string... only other supported type was a numeric, 213 | // which would have been sent as a float value. 214 | floatValue, err := strconv.ParseFloat(data.ID, 64) 215 | if err != nil { 216 | // Could not convert the value in the "id" attr to a float 217 | er = ErrBadJSONAPIID 218 | break 219 | } 220 | 221 | // Convert the numeric float to one of the supported ID numeric types 222 | // (int[8,16,32,64] or uint[8,16,32,64]) 223 | idValue, err := handleNumeric(floatValue, fieldType.Type, fieldValue) 224 | if err != nil { 225 | // We had a JSON float (numeric), but our field was not one of the 226 | // allowed numeric types 227 | er = ErrBadJSONAPIID 228 | break 229 | } 230 | 231 | assign(fieldValue, idValue) 232 | } else if annotation == annotationClientID { 233 | if data.ClientID == "" { 234 | continue 235 | } 236 | 237 | fieldValue.Set(reflect.ValueOf(data.ClientID)) 238 | } else if annotation == annotationAttribute { 239 | attributes := data.Attributes 240 | 241 | if attributes == nil || len(data.Attributes) == 0 { 242 | continue 243 | } 244 | 245 | attribute := attributes[args[1]] 246 | 247 | // continue if the attribute was not included in the request 248 | if attribute == nil { 249 | continue 250 | } 251 | 252 | structField := fieldType 253 | value, err := unmarshalAttribute(attribute, args, structField, fieldValue) 254 | if err != nil { 255 | er = err 256 | break 257 | } 258 | 259 | assign(fieldValue, value) 260 | } else if annotation == annotationRelation { 261 | isSlice := fieldValue.Type().Kind() == reflect.Slice 262 | 263 | if data.Relationships == nil || data.Relationships[args[1]] == nil { 264 | continue 265 | } 266 | 267 | if isSlice { 268 | // to-many relationship 269 | relationship := new(RelationshipManyNode) 270 | 271 | buf := bytes.NewBuffer(nil) 272 | 273 | json.NewEncoder(buf).Encode(data.Relationships[args[1]]) 274 | json.NewDecoder(buf).Decode(relationship) 275 | 276 | data := relationship.Data 277 | models := reflect.New(fieldValue.Type()).Elem() 278 | 279 | for _, n := range data { 280 | m := reflect.New(fieldValue.Type().Elem().Elem()) 281 | 282 | if err := unmarshalNode( 283 | fullNode(n, included), 284 | m, 285 | included, 286 | ); err != nil { 287 | er = err 288 | break 289 | } 290 | 291 | models = reflect.Append(models, m) 292 | } 293 | 294 | fieldValue.Set(models) 295 | } else { 296 | // to-one relationships 297 | relationship := new(RelationshipOneNode) 298 | 299 | buf := bytes.NewBuffer(nil) 300 | 301 | json.NewEncoder(buf).Encode( 302 | data.Relationships[args[1]], 303 | ) 304 | json.NewDecoder(buf).Decode(relationship) 305 | 306 | /* 307 | http://jsonapi.org/format/#document-resource-object-relationships 308 | http://jsonapi.org/format/#document-resource-object-linkage 309 | relationship can have a data node set to null (e.g. to disassociate the relationship) 310 | so unmarshal and set fieldValue only if data obj is not null 311 | */ 312 | if relationship.Data == nil { 313 | continue 314 | } 315 | 316 | m := reflect.New(fieldValue.Type().Elem()) 317 | if err := unmarshalNode( 318 | fullNode(relationship.Data, included), 319 | m, 320 | included, 321 | ); err != nil { 322 | er = err 323 | break 324 | } 325 | 326 | fieldValue.Set(m) 327 | 328 | } 329 | 330 | } else { 331 | er = fmt.Errorf(unsupportedStructTagMsg, annotation) 332 | } 333 | } 334 | 335 | return er 336 | } 337 | 338 | func fullNode(n *Node, included *map[string]*Node) *Node { 339 | includedKey := fmt.Sprintf("%s,%s", n.Type, n.ID) 340 | 341 | if included != nil && (*included)[includedKey] != nil { 342 | return (*included)[includedKey] 343 | } 344 | 345 | return n 346 | } 347 | 348 | // assign will take the value specified and assign it to the field; if 349 | // field is expecting a ptr assign will assign a ptr. 350 | func assign(field, value reflect.Value) { 351 | value = reflect.Indirect(value) 352 | 353 | if field.Kind() == reflect.Ptr { 354 | // initialize pointer so it's value 355 | // can be set by assignValue 356 | field.Set(reflect.New(field.Type().Elem())) 357 | field = field.Elem() 358 | 359 | } 360 | 361 | assignValue(field, value) 362 | } 363 | 364 | // assign assigns the specified value to the field, 365 | // expecting both values not to be pointer types. 366 | func assignValue(field, value reflect.Value) { 367 | switch field.Kind() { 368 | case reflect.Int, reflect.Int8, reflect.Int16, 369 | reflect.Int32, reflect.Int64: 370 | field.SetInt(value.Int()) 371 | case reflect.Uint, reflect.Uint8, reflect.Uint16, 372 | reflect.Uint32, reflect.Uint64, reflect.Uintptr: 373 | field.SetUint(value.Uint()) 374 | case reflect.Float32, reflect.Float64: 375 | field.SetFloat(value.Float()) 376 | case reflect.String: 377 | field.SetString(value.String()) 378 | case reflect.Bool: 379 | field.SetBool(value.Bool()) 380 | default: 381 | field.Set(value) 382 | } 383 | } 384 | 385 | func unmarshalAttribute( 386 | attribute interface{}, 387 | args []string, 388 | structField reflect.StructField, 389 | fieldValue reflect.Value) (value reflect.Value, err error) { 390 | value = reflect.ValueOf(attribute) 391 | fieldType := structField.Type 392 | 393 | // Handle field of type []string 394 | if fieldValue.Type() == reflect.TypeOf([]string{}) { 395 | value, err = handleStringSlice(attribute) 396 | return 397 | } 398 | 399 | // Handle field of type time.Time 400 | if fieldValue.Type() == reflect.TypeOf(time.Time{}) || 401 | fieldValue.Type() == reflect.TypeOf(new(time.Time)) { 402 | value, err = handleTime(attribute, args, fieldValue) 403 | return 404 | } 405 | 406 | // Handle field of type struct 407 | if fieldValue.Type().Kind() == reflect.Struct { 408 | value, err = handleStruct(attribute, fieldValue) 409 | return 410 | } 411 | 412 | // Handle field containing slice of structs 413 | if fieldValue.Type().Kind() == reflect.Slice && 414 | reflect.TypeOf(fieldValue.Interface()).Elem().Kind() == reflect.Struct { 415 | value, err = handleStructSlice(attribute, fieldValue) 416 | return 417 | } 418 | 419 | // JSON value was a float (numeric) 420 | if value.Kind() == reflect.Float64 { 421 | value, err = handleNumeric(attribute, fieldType, fieldValue) 422 | return 423 | } 424 | 425 | // Field was a Pointer type 426 | if fieldValue.Kind() == reflect.Ptr { 427 | value, err = handlePointer(attribute, args, fieldType, fieldValue, structField) 428 | return 429 | } 430 | 431 | // As a final catch-all, ensure types line up to avoid a runtime panic. 432 | if fieldValue.Kind() != value.Kind() { 433 | err = ErrInvalidType 434 | return 435 | } 436 | 437 | return 438 | } 439 | 440 | func handleStringSlice(attribute interface{}) (reflect.Value, error) { 441 | v := reflect.ValueOf(attribute) 442 | values := make([]string, v.Len()) 443 | for i := 0; i < v.Len(); i++ { 444 | values[i] = v.Index(i).Interface().(string) 445 | } 446 | 447 | return reflect.ValueOf(values), nil 448 | } 449 | 450 | func handleTime(attribute interface{}, args []string, fieldValue reflect.Value) (reflect.Value, error) { 451 | var isISO8601, isRFC3339 bool 452 | v := reflect.ValueOf(attribute) 453 | 454 | if len(args) > 2 { 455 | for _, arg := range args[2:] { 456 | if arg == annotationISO8601 { 457 | isISO8601 = true 458 | } else if arg == annotationRFC3339 { 459 | isRFC3339 = true 460 | } 461 | } 462 | } 463 | 464 | if isISO8601 { 465 | if v.Kind() != reflect.String { 466 | return reflect.ValueOf(time.Now()), ErrInvalidISO8601 467 | } 468 | 469 | t, err := time.Parse(iso8601TimeFormat, v.Interface().(string)) 470 | if err != nil { 471 | return reflect.ValueOf(time.Now()), ErrInvalidISO8601 472 | } 473 | 474 | if fieldValue.Kind() == reflect.Ptr { 475 | return reflect.ValueOf(&t), nil 476 | } 477 | 478 | return reflect.ValueOf(t), nil 479 | } 480 | 481 | if isRFC3339 { 482 | if v.Kind() != reflect.String { 483 | return reflect.ValueOf(time.Now()), ErrInvalidRFC3339 484 | } 485 | 486 | t, err := time.Parse(time.RFC3339, v.Interface().(string)) 487 | if err != nil { 488 | return reflect.ValueOf(time.Now()), ErrInvalidRFC3339 489 | } 490 | 491 | if fieldValue.Kind() == reflect.Ptr { 492 | return reflect.ValueOf(&t), nil 493 | } 494 | 495 | return reflect.ValueOf(t), nil 496 | } 497 | 498 | var at int64 499 | 500 | if v.Kind() == reflect.Float64 { 501 | at = int64(v.Interface().(float64)) 502 | } else if v.Kind() == reflect.Int { 503 | at = v.Int() 504 | } else { 505 | return reflect.ValueOf(time.Now()), ErrInvalidTime 506 | } 507 | 508 | t := time.Unix(at, 0) 509 | 510 | return reflect.ValueOf(t), nil 511 | } 512 | 513 | func handleNumeric( 514 | attribute interface{}, 515 | fieldType reflect.Type, 516 | fieldValue reflect.Value) (reflect.Value, error) { 517 | v := reflect.ValueOf(attribute) 518 | floatValue := v.Interface().(float64) 519 | 520 | var kind reflect.Kind 521 | if fieldValue.Kind() == reflect.Ptr { 522 | kind = fieldType.Elem().Kind() 523 | } else { 524 | kind = fieldType.Kind() 525 | } 526 | 527 | var numericValue reflect.Value 528 | 529 | switch kind { 530 | case reflect.Int: 531 | n := int(floatValue) 532 | numericValue = reflect.ValueOf(&n) 533 | case reflect.Int8: 534 | n := int8(floatValue) 535 | numericValue = reflect.ValueOf(&n) 536 | case reflect.Int16: 537 | n := int16(floatValue) 538 | numericValue = reflect.ValueOf(&n) 539 | case reflect.Int32: 540 | n := int32(floatValue) 541 | numericValue = reflect.ValueOf(&n) 542 | case reflect.Int64: 543 | n := int64(floatValue) 544 | numericValue = reflect.ValueOf(&n) 545 | case reflect.Uint: 546 | n := uint(floatValue) 547 | numericValue = reflect.ValueOf(&n) 548 | case reflect.Uint8: 549 | n := uint8(floatValue) 550 | numericValue = reflect.ValueOf(&n) 551 | case reflect.Uint16: 552 | n := uint16(floatValue) 553 | numericValue = reflect.ValueOf(&n) 554 | case reflect.Uint32: 555 | n := uint32(floatValue) 556 | numericValue = reflect.ValueOf(&n) 557 | case reflect.Uint64: 558 | n := uint64(floatValue) 559 | numericValue = reflect.ValueOf(&n) 560 | case reflect.Float32: 561 | n := float32(floatValue) 562 | numericValue = reflect.ValueOf(&n) 563 | case reflect.Float64: 564 | n := floatValue 565 | numericValue = reflect.ValueOf(&n) 566 | default: 567 | return reflect.Value{}, ErrUnknownFieldNumberType 568 | } 569 | 570 | return numericValue, nil 571 | } 572 | 573 | func handlePointer( 574 | attribute interface{}, 575 | args []string, 576 | fieldType reflect.Type, 577 | fieldValue reflect.Value, 578 | structField reflect.StructField) (reflect.Value, error) { 579 | t := fieldValue.Type() 580 | var concreteVal reflect.Value 581 | 582 | switch cVal := attribute.(type) { 583 | case string: 584 | concreteVal = reflect.ValueOf(&cVal) 585 | case bool: 586 | concreteVal = reflect.ValueOf(&cVal) 587 | case complex64, complex128, uintptr: 588 | concreteVal = reflect.ValueOf(&cVal) 589 | case map[string]interface{}: 590 | var err error 591 | concreteVal, err = handleStruct(attribute, fieldValue) 592 | if err != nil { 593 | return reflect.Value{}, newErrUnsupportedPtrType( 594 | reflect.ValueOf(attribute), fieldType, structField) 595 | } 596 | return concreteVal, err 597 | default: 598 | return reflect.Value{}, newErrUnsupportedPtrType( 599 | reflect.ValueOf(attribute), fieldType, structField) 600 | } 601 | 602 | if t != concreteVal.Type() { 603 | return reflect.Value{}, newErrUnsupportedPtrType( 604 | reflect.ValueOf(attribute), fieldType, structField) 605 | } 606 | 607 | return concreteVal, nil 608 | } 609 | 610 | func handleStruct( 611 | attribute interface{}, 612 | fieldValue reflect.Value) (reflect.Value, error) { 613 | 614 | data, err := json.Marshal(attribute) 615 | if err != nil { 616 | return reflect.Value{}, err 617 | } 618 | 619 | node := new(Node) 620 | if err := json.Unmarshal(data, &node.Attributes); err != nil { 621 | return reflect.Value{}, err 622 | } 623 | 624 | var model reflect.Value 625 | if fieldValue.Kind() == reflect.Ptr { 626 | model = reflect.New(fieldValue.Type().Elem()) 627 | } else { 628 | model = reflect.New(fieldValue.Type()) 629 | } 630 | 631 | if err := unmarshalNode(node, model, nil); err != nil { 632 | return reflect.Value{}, err 633 | } 634 | 635 | return model, nil 636 | } 637 | 638 | func handleStructSlice( 639 | attribute interface{}, 640 | fieldValue reflect.Value) (reflect.Value, error) { 641 | models := reflect.New(fieldValue.Type()).Elem() 642 | dataMap := reflect.ValueOf(attribute).Interface().([]interface{}) 643 | for _, data := range dataMap { 644 | model := reflect.New(fieldValue.Type().Elem()).Elem() 645 | 646 | value, err := handleStruct(data, model) 647 | 648 | if err != nil { 649 | continue 650 | } 651 | 652 | models = reflect.Append(models, reflect.Indirect(value)) 653 | } 654 | 655 | return models, nil 656 | } 657 | -------------------------------------------------------------------------------- /response_test.go: -------------------------------------------------------------------------------- 1 | package jsonapi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | "sort" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestMarshalPayload(t *testing.T) { 14 | book := &Book{ID: 1} 15 | books := []*Book{book, {ID: 2}} 16 | var jsonData map[string]interface{} 17 | 18 | // One 19 | out1 := bytes.NewBuffer(nil) 20 | MarshalPayload(out1, book) 21 | 22 | if err := json.Unmarshal(out1.Bytes(), &jsonData); err != nil { 23 | t.Fatal(err) 24 | } 25 | if _, ok := jsonData["data"].(map[string]interface{}); !ok { 26 | t.Fatalf("data key did not contain an Hash/Dict/Map") 27 | } 28 | 29 | // Many 30 | out2 := bytes.NewBuffer(nil) 31 | MarshalPayload(out2, books) 32 | 33 | if err := json.Unmarshal(out2.Bytes(), &jsonData); err != nil { 34 | t.Fatal(err) 35 | } 36 | if _, ok := jsonData["data"].([]interface{}); !ok { 37 | t.Fatalf("data key did not contain an Array") 38 | } 39 | } 40 | 41 | func TestMarshalPayloadWithNulls(t *testing.T) { 42 | 43 | books := []*Book{nil, {ID: 101}, nil} 44 | var jsonData map[string]interface{} 45 | 46 | out := bytes.NewBuffer(nil) 47 | if err := MarshalPayload(out, books); err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { 52 | t.Fatal(err) 53 | } 54 | raw, ok := jsonData["data"] 55 | if !ok { 56 | t.Fatalf("data key does not exist") 57 | } 58 | arr, ok := raw.([]interface{}) 59 | if !ok { 60 | t.Fatalf("data is not an Array") 61 | } 62 | for i := 0; i < len(arr); i++ { 63 | if books[i] == nil && arr[i] != nil || 64 | books[i] != nil && arr[i] == nil { 65 | t.Fatalf("restored data is not equal to source") 66 | } 67 | } 68 | } 69 | 70 | func TestMarshal_attrStringSlice(t *testing.T) { 71 | tags := []string{"fiction", "sale"} 72 | b := &Book{ID: 1, Tags: tags} 73 | 74 | out := bytes.NewBuffer(nil) 75 | if err := MarshalPayload(out, b); err != nil { 76 | t.Fatal(err) 77 | } 78 | 79 | var jsonData map[string]interface{} 80 | if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { 81 | t.Fatal(err) 82 | } 83 | 84 | jsonTags := jsonData["data"].(map[string]interface{})["attributes"].(map[string]interface{})["tags"].([]interface{}) 85 | if e, a := len(tags), len(jsonTags); e != a { 86 | t.Fatalf("Was expecting tags of length %d got %d", e, a) 87 | } 88 | 89 | // Convert from []interface{} to []string 90 | jsonTagsStrings := []string{} 91 | for _, tag := range jsonTags { 92 | jsonTagsStrings = append(jsonTagsStrings, tag.(string)) 93 | } 94 | 95 | // Sort both 96 | sort.Strings(jsonTagsStrings) 97 | sort.Strings(tags) 98 | 99 | for i, tag := range tags { 100 | if e, a := tag, jsonTagsStrings[i]; e != a { 101 | t.Fatalf("At index %d, was expecting %s got %s", i, e, a) 102 | } 103 | } 104 | } 105 | 106 | func TestWithoutOmitsEmptyAnnotationOnRelation(t *testing.T) { 107 | blog := &Blog{} 108 | 109 | out := bytes.NewBuffer(nil) 110 | if err := MarshalPayload(out, blog); err != nil { 111 | t.Fatal(err) 112 | } 113 | 114 | var jsonData map[string]interface{} 115 | if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { 116 | t.Fatal(err) 117 | } 118 | relationships := jsonData["data"].(map[string]interface{})["relationships"].(map[string]interface{}) 119 | 120 | // Verifiy the "posts" relation was an empty array 121 | posts, ok := relationships["posts"] 122 | if !ok { 123 | t.Fatal("Was expecting the data.relationships.posts key/value to have been present") 124 | } 125 | postsMap, ok := posts.(map[string]interface{}) 126 | if !ok { 127 | t.Fatal("data.relationships.posts was not a map") 128 | } 129 | postsData, ok := postsMap["data"] 130 | if !ok { 131 | t.Fatal("Was expecting the data.relationships.posts.data key/value to have been present") 132 | } 133 | postsDataSlice, ok := postsData.([]interface{}) 134 | if !ok { 135 | t.Fatal("data.relationships.posts.data was not a slice []") 136 | } 137 | if len(postsDataSlice) != 0 { 138 | t.Fatal("Was expecting the data.relationships.posts.data value to have been an empty array []") 139 | } 140 | 141 | // Verifiy the "current_post" was a null 142 | currentPost, postExists := relationships["current_post"] 143 | if !postExists { 144 | t.Fatal("Was expecting the data.relationships.current_post key/value to have NOT been omitted") 145 | } 146 | currentPostMap, ok := currentPost.(map[string]interface{}) 147 | if !ok { 148 | t.Fatal("data.relationships.current_post was not a map") 149 | } 150 | currentPostData, ok := currentPostMap["data"] 151 | if !ok { 152 | t.Fatal("Was expecting the data.relationships.current_post.data key/value to have been present") 153 | } 154 | if currentPostData != nil { 155 | t.Fatal("Was expecting the data.relationships.current_post.data value to have been nil/null") 156 | } 157 | } 158 | 159 | func TestWithOmitsEmptyAnnotationOnRelation(t *testing.T) { 160 | type BlogOptionalPosts struct { 161 | ID int `jsonapi:"primary,blogs"` 162 | Title string `jsonapi:"attr,title"` 163 | Posts []*Post `jsonapi:"relation,posts,omitempty"` 164 | CurrentPost *Post `jsonapi:"relation,current_post,omitempty"` 165 | } 166 | 167 | blog := &BlogOptionalPosts{ID: 999} 168 | 169 | out := bytes.NewBuffer(nil) 170 | if err := MarshalPayload(out, blog); err != nil { 171 | t.Fatal(err) 172 | } 173 | 174 | var jsonData map[string]interface{} 175 | if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { 176 | t.Fatal(err) 177 | } 178 | payload := jsonData["data"].(map[string]interface{}) 179 | 180 | // Verify relationship was NOT set 181 | if val, exists := payload["relationships"]; exists { 182 | t.Fatalf("Was expecting the data.relationships key/value to have been empty - it was not and had a value of %v", val) 183 | } 184 | } 185 | 186 | func TestWithOmitsEmptyAnnotationOnRelation_MixedData(t *testing.T) { 187 | type BlogOptionalPosts struct { 188 | ID int `jsonapi:"primary,blogs"` 189 | Title string `jsonapi:"attr,title"` 190 | Posts []*Post `jsonapi:"relation,posts,omitempty"` 191 | CurrentPost *Post `jsonapi:"relation,current_post,omitempty"` 192 | } 193 | 194 | blog := &BlogOptionalPosts{ 195 | ID: 999, 196 | CurrentPost: &Post{ 197 | ID: 123, 198 | }, 199 | } 200 | 201 | out := bytes.NewBuffer(nil) 202 | if err := MarshalPayload(out, blog); err != nil { 203 | t.Fatal(err) 204 | } 205 | 206 | var jsonData map[string]interface{} 207 | if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { 208 | t.Fatal(err) 209 | } 210 | payload := jsonData["data"].(map[string]interface{}) 211 | 212 | // Verify relationship was set 213 | if _, exists := payload["relationships"]; !exists { 214 | t.Fatal("Was expecting the data.relationships key/value to have NOT been empty") 215 | } 216 | 217 | relationships := payload["relationships"].(map[string]interface{}) 218 | 219 | // Verify the relationship was not omitted, and is not null 220 | if val, exists := relationships["current_post"]; !exists { 221 | t.Fatal("Was expecting the data.relationships.current_post key/value to have NOT been omitted") 222 | } else if val.(map[string]interface{})["data"] == nil { 223 | t.Fatal("Was expecting the data.relationships.current_post value to have NOT been nil/null") 224 | } 225 | } 226 | 227 | func TestWithOmitsEmptyAnnotationOnAttribute(t *testing.T) { 228 | type Phone struct { 229 | Number string `json:"number"` 230 | } 231 | 232 | type Address struct { 233 | City string `json:"city"` 234 | Street string `json:"street"` 235 | } 236 | 237 | type Tags map[string]int 238 | 239 | type Author struct { 240 | ID int `jsonapi:"primary,authors"` 241 | Name string `jsonapi:"attr,title"` 242 | Phones []*Phone `jsonapi:"attr,phones,omitempty"` 243 | Address *Address `jsonapi:"attr,address,omitempty"` 244 | Tags Tags `jsonapi:"attr,tags,omitempty"` 245 | } 246 | 247 | author := &Author{ 248 | ID: 999, 249 | Name: "Igor", 250 | Phones: nil, // should be omitted 251 | Address: nil, // should be omitted 252 | Tags: Tags{"dogs": 1, "cats": 2}, // should not be omitted 253 | } 254 | 255 | out := bytes.NewBuffer(nil) 256 | if err := MarshalPayload(out, author); err != nil { 257 | t.Fatal(err) 258 | } 259 | 260 | var jsonData map[string]interface{} 261 | if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { 262 | t.Fatal(err) 263 | } 264 | 265 | // Verify that there is no field "phones" in attributes 266 | payload := jsonData["data"].(map[string]interface{}) 267 | attributes := payload["attributes"].(map[string]interface{}) 268 | if _, ok := attributes["title"]; !ok { 269 | t.Fatal("Was expecting the data.attributes.title to have NOT been omitted") 270 | } 271 | if _, ok := attributes["phones"]; ok { 272 | t.Fatal("Was expecting the data.attributes.phones to have been omitted") 273 | } 274 | if _, ok := attributes["address"]; ok { 275 | t.Fatal("Was expecting the data.attributes.phones to have been omitted") 276 | } 277 | if _, ok := attributes["tags"]; !ok { 278 | t.Fatal("Was expecting the data.attributes.tags to have NOT been omitted") 279 | } 280 | } 281 | 282 | func TestMarshalIDPtr(t *testing.T) { 283 | id, make, model := "123e4567-e89b-12d3-a456-426655440000", "Ford", "Mustang" 284 | car := &Car{ 285 | ID: &id, 286 | Make: &make, 287 | Model: &model, 288 | } 289 | 290 | out := bytes.NewBuffer(nil) 291 | if err := MarshalPayload(out, car); err != nil { 292 | t.Fatal(err) 293 | } 294 | 295 | var jsonData map[string]interface{} 296 | if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { 297 | t.Fatal(err) 298 | } 299 | data := jsonData["data"].(map[string]interface{}) 300 | // attributes := data["attributes"].(map[string]interface{}) 301 | 302 | // Verify that the ID was sent 303 | val, exists := data["id"] 304 | if !exists { 305 | t.Fatal("Was expecting the data.id member to exist") 306 | } 307 | if val != id { 308 | t.Fatalf("Was expecting the data.id member to be `%s`, got `%s`", id, val) 309 | } 310 | } 311 | 312 | func TestMarshalOnePayload_omitIDString(t *testing.T) { 313 | type Foo struct { 314 | ID string `jsonapi:"primary,foo"` 315 | Title string `jsonapi:"attr,title"` 316 | } 317 | 318 | foo := &Foo{Title: "Foo"} 319 | out := bytes.NewBuffer(nil) 320 | if err := MarshalPayload(out, foo); err != nil { 321 | t.Fatal(err) 322 | } 323 | 324 | var jsonData map[string]interface{} 325 | if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { 326 | t.Fatal(err) 327 | } 328 | payload := jsonData["data"].(map[string]interface{}) 329 | 330 | // Verify that empty ID of type string gets omitted. See: 331 | // https://github.com/google/jsonapi/issues/83#issuecomment-285611425 332 | _, ok := payload["id"] 333 | if ok { 334 | t.Fatal("Was expecting the data.id member to be omitted") 335 | } 336 | } 337 | 338 | func TestMarshall_invalidIDType(t *testing.T) { 339 | type badIDStruct struct { 340 | ID *bool `jsonapi:"primary,cars"` 341 | } 342 | id := true 343 | o := &badIDStruct{ID: &id} 344 | 345 | out := bytes.NewBuffer(nil) 346 | if err := MarshalPayload(out, o); err != ErrBadJSONAPIID { 347 | t.Fatalf( 348 | "Was expecting a `%s` error, got `%s`", ErrBadJSONAPIID, err, 349 | ) 350 | } 351 | } 352 | 353 | func TestOmitsEmptyAnnotation(t *testing.T) { 354 | book := &Book{ 355 | Author: "aren55555", 356 | PublishedAt: time.Now().AddDate(0, -1, 0), 357 | } 358 | 359 | out := bytes.NewBuffer(nil) 360 | if err := MarshalPayload(out, book); err != nil { 361 | t.Fatal(err) 362 | } 363 | 364 | var jsonData map[string]interface{} 365 | if err := json.Unmarshal(out.Bytes(), &jsonData); err != nil { 366 | t.Fatal(err) 367 | } 368 | attributes := jsonData["data"].(map[string]interface{})["attributes"].(map[string]interface{}) 369 | 370 | // Verify that the specifically omitted field were omitted 371 | if val, exists := attributes["title"]; exists { 372 | t.Fatalf("Was expecting the data.attributes.title key/value to have been omitted - it was not and had a value of %v", val) 373 | } 374 | if val, exists := attributes["pages"]; exists { 375 | t.Fatalf("Was expecting the data.attributes.pages key/value to have been omitted - it was not and had a value of %v", val) 376 | } 377 | 378 | // Verify the implicitly omitted fields were omitted 379 | if val, exists := attributes["PublishedAt"]; exists { 380 | t.Fatalf("Was expecting the data.attributes.PublishedAt key/value to have been implicitly omitted - it was not and had a value of %v", val) 381 | } 382 | 383 | // Verify the unset fields were not omitted 384 | if _, exists := attributes["isbn"]; !exists { 385 | t.Fatal("Was expecting the data.attributes.isbn key/value to have NOT been omitted") 386 | } 387 | } 388 | 389 | func TestHasPrimaryAnnotation(t *testing.T) { 390 | testModel := &Blog{ 391 | ID: 5, 392 | Title: "Title 1", 393 | CreatedAt: time.Now(), 394 | } 395 | 396 | out := bytes.NewBuffer(nil) 397 | if err := MarshalPayload(out, testModel); err != nil { 398 | t.Fatal(err) 399 | } 400 | 401 | resp := new(OnePayload) 402 | 403 | if err := json.NewDecoder(out).Decode(resp); err != nil { 404 | t.Fatal(err) 405 | } 406 | 407 | data := resp.Data 408 | 409 | if data.Type != "blogs" { 410 | t.Fatalf("type should have been blogs, got %s", data.Type) 411 | } 412 | 413 | if data.ID != "5" { 414 | t.Fatalf("ID not transferred") 415 | } 416 | } 417 | 418 | func TestSupportsAttributes(t *testing.T) { 419 | testModel := &Blog{ 420 | ID: 5, 421 | Title: "Title 1", 422 | CreatedAt: time.Now(), 423 | } 424 | 425 | out := bytes.NewBuffer(nil) 426 | if err := MarshalPayload(out, testModel); err != nil { 427 | t.Fatal(err) 428 | } 429 | 430 | resp := new(OnePayload) 431 | if err := json.NewDecoder(out).Decode(resp); err != nil { 432 | t.Fatal(err) 433 | } 434 | 435 | data := resp.Data 436 | 437 | if data.Attributes == nil { 438 | t.Fatalf("Expected attributes") 439 | } 440 | 441 | if data.Attributes["title"] != "Title 1" { 442 | t.Fatalf("Attributes hash not populated using tags correctly") 443 | } 444 | } 445 | 446 | func TestOmitsZeroTimes(t *testing.T) { 447 | testModel := &Blog{ 448 | ID: 5, 449 | Title: "Title 1", 450 | CreatedAt: time.Time{}, 451 | } 452 | 453 | out := bytes.NewBuffer(nil) 454 | if err := MarshalPayload(out, testModel); err != nil { 455 | t.Fatal(err) 456 | } 457 | 458 | resp := new(OnePayload) 459 | if err := json.NewDecoder(out).Decode(resp); err != nil { 460 | t.Fatal(err) 461 | } 462 | 463 | data := resp.Data 464 | 465 | if data.Attributes == nil { 466 | t.Fatalf("Expected attributes") 467 | } 468 | 469 | if data.Attributes["created_at"] != nil { 470 | t.Fatalf("Created at was serialized even though it was a zero Time") 471 | } 472 | } 473 | 474 | func TestMarshal_Times(t *testing.T) { 475 | aTime := time.Date(2016, 8, 17, 8, 27, 12, 23849, time.UTC) 476 | 477 | for _, tc := range []struct { 478 | desc string 479 | input *TimestampModel 480 | verification func(data map[string]interface{}) error 481 | }{ 482 | { 483 | desc: "default_byValue", 484 | input: &TimestampModel{ 485 | ID: 5, 486 | DefaultV: aTime, 487 | }, 488 | verification: func(root map[string]interface{}) error { 489 | v := root["data"].(map[string]interface{})["attributes"].(map[string]interface{})["defaultv"].(float64) 490 | if got, want := int64(v), aTime.Unix(); got != want { 491 | return fmt.Errorf("got %v, want %v", got, want) 492 | } 493 | return nil 494 | }, 495 | }, 496 | { 497 | desc: "default_byPointer", 498 | input: &TimestampModel{ 499 | ID: 5, 500 | DefaultP: &aTime, 501 | }, 502 | verification: func(root map[string]interface{}) error { 503 | v := root["data"].(map[string]interface{})["attributes"].(map[string]interface{})["defaultp"].(float64) 504 | if got, want := int64(v), aTime.Unix(); got != want { 505 | return fmt.Errorf("got %v, want %v", got, want) 506 | } 507 | return nil 508 | }, 509 | }, 510 | { 511 | desc: "iso8601_byValue", 512 | input: &TimestampModel{ 513 | ID: 5, 514 | ISO8601V: aTime, 515 | }, 516 | verification: func(root map[string]interface{}) error { 517 | v := root["data"].(map[string]interface{})["attributes"].(map[string]interface{})["iso8601v"].(string) 518 | if got, want := v, aTime.UTC().Format(iso8601TimeFormat); got != want { 519 | return fmt.Errorf("got %v, want %v", got, want) 520 | } 521 | return nil 522 | }, 523 | }, 524 | { 525 | desc: "iso8601_byPointer", 526 | input: &TimestampModel{ 527 | ID: 5, 528 | ISO8601P: &aTime, 529 | }, 530 | verification: func(root map[string]interface{}) error { 531 | v := root["data"].(map[string]interface{})["attributes"].(map[string]interface{})["iso8601p"].(string) 532 | if got, want := v, aTime.UTC().Format(iso8601TimeFormat); got != want { 533 | return fmt.Errorf("got %v, want %v", got, want) 534 | } 535 | return nil 536 | }, 537 | }, 538 | { 539 | desc: "rfc3339_byValue", 540 | input: &TimestampModel{ 541 | ID: 5, 542 | RFC3339V: aTime, 543 | }, 544 | verification: func(root map[string]interface{}) error { 545 | v := root["data"].(map[string]interface{})["attributes"].(map[string]interface{})["rfc3339v"].(string) 546 | if got, want := v, aTime.UTC().Format(time.RFC3339); got != want { 547 | return fmt.Errorf("got %v, want %v", got, want) 548 | } 549 | return nil 550 | }, 551 | }, 552 | { 553 | desc: "rfc3339_byPointer", 554 | input: &TimestampModel{ 555 | ID: 5, 556 | RFC3339P: &aTime, 557 | }, 558 | verification: func(root map[string]interface{}) error { 559 | v := root["data"].(map[string]interface{})["attributes"].(map[string]interface{})["rfc3339p"].(string) 560 | if got, want := v, aTime.UTC().Format(time.RFC3339); got != want { 561 | return fmt.Errorf("got %v, want %v", got, want) 562 | } 563 | return nil 564 | }, 565 | }, 566 | } { 567 | t.Run(tc.desc, func(t *testing.T) { 568 | out := bytes.NewBuffer(nil) 569 | if err := MarshalPayload(out, tc.input); err != nil { 570 | t.Fatal(err) 571 | } 572 | // Use the standard JSON library to traverse the genereated JSON payload. 573 | data := map[string]interface{}{} 574 | json.Unmarshal(out.Bytes(), &data) 575 | if tc.verification != nil { 576 | if err := tc.verification(data); err != nil { 577 | t.Fatal(err) 578 | } 579 | } 580 | }) 581 | } 582 | } 583 | 584 | func TestSupportsLinkable(t *testing.T) { 585 | testModel := &Blog{ 586 | ID: 5, 587 | Title: "Title 1", 588 | CreatedAt: time.Now(), 589 | } 590 | 591 | out := bytes.NewBuffer(nil) 592 | if err := MarshalPayload(out, testModel); err != nil { 593 | t.Fatal(err) 594 | } 595 | 596 | resp := new(OnePayload) 597 | if err := json.NewDecoder(out).Decode(resp); err != nil { 598 | t.Fatal(err) 599 | } 600 | 601 | data := resp.Data 602 | 603 | if data.Links == nil { 604 | t.Fatal("Expected data.links") 605 | } 606 | links := *data.Links 607 | 608 | self, hasSelf := links["self"] 609 | if !hasSelf { 610 | t.Fatal("Expected 'self' link to be present") 611 | } 612 | if _, isString := self.(string); !isString { 613 | t.Fatal("Expected 'self' to contain a string") 614 | } 615 | 616 | comments, hasComments := links["comments"] 617 | if !hasComments { 618 | t.Fatal("expect 'comments' to be present") 619 | } 620 | commentsMap, isMap := comments.(map[string]interface{}) 621 | if !isMap { 622 | t.Fatal("Expected 'comments' to contain a map") 623 | } 624 | 625 | commentsHref, hasHref := commentsMap["href"] 626 | if !hasHref { 627 | t.Fatal("Expect 'comments' to contain an 'href' key/value") 628 | } 629 | if _, isString := commentsHref.(string); !isString { 630 | t.Fatal("Expected 'href' to contain a string") 631 | } 632 | 633 | commentsMeta, hasMeta := commentsMap["meta"] 634 | if !hasMeta { 635 | t.Fatal("Expect 'comments' to contain a 'meta' key/value") 636 | } 637 | commentsMetaMap, isMap := commentsMeta.(map[string]interface{}) 638 | if !isMap { 639 | t.Fatal("Expected 'comments' to contain a map") 640 | } 641 | 642 | commentsMetaObject := Meta(commentsMetaMap) 643 | countsMap, isMap := commentsMetaObject["counts"].(map[string]interface{}) 644 | if !isMap { 645 | t.Fatal("Expected 'counts' to contain a map") 646 | } 647 | for k, v := range countsMap { 648 | if _, isNum := v.(float64); !isNum { 649 | t.Fatalf("Exepected value at '%s' to be a numeric (float64)", k) 650 | } 651 | } 652 | } 653 | 654 | func TestInvalidLinkable(t *testing.T) { 655 | testModel := &BadComment{ 656 | ID: 5, 657 | Body: "Hello World", 658 | } 659 | 660 | out := bytes.NewBuffer(nil) 661 | if err := MarshalPayload(out, testModel); err == nil { 662 | t.Fatal("Was expecting an error") 663 | } 664 | } 665 | 666 | func TestSupportsMetable(t *testing.T) { 667 | testModel := &Blog{ 668 | ID: 5, 669 | Title: "Title 1", 670 | CreatedAt: time.Now(), 671 | } 672 | 673 | out := bytes.NewBuffer(nil) 674 | if err := MarshalPayload(out, testModel); err != nil { 675 | t.Fatal(err) 676 | } 677 | 678 | resp := new(OnePayload) 679 | if err := json.NewDecoder(out).Decode(resp); err != nil { 680 | t.Fatal(err) 681 | } 682 | 683 | data := resp.Data 684 | if data.Meta == nil { 685 | t.Fatalf("Expected data.meta") 686 | } 687 | 688 | meta := Meta(*data.Meta) 689 | if e, a := "extra details regarding the blog", meta["detail"]; e != a { 690 | t.Fatalf("Was expecting meta.detail to be %q, got %q", e, a) 691 | } 692 | } 693 | 694 | func TestRelations(t *testing.T) { 695 | testModel := testBlog() 696 | 697 | out := bytes.NewBuffer(nil) 698 | if err := MarshalPayload(out, testModel); err != nil { 699 | t.Fatal(err) 700 | } 701 | 702 | resp := new(OnePayload) 703 | if err := json.NewDecoder(out).Decode(resp); err != nil { 704 | t.Fatal(err) 705 | } 706 | 707 | relations := resp.Data.Relationships 708 | 709 | if relations == nil { 710 | t.Fatalf("Relationships were not materialized") 711 | } 712 | 713 | if relations["posts"] == nil { 714 | t.Fatalf("Posts relationship was not materialized") 715 | } else { 716 | if relations["posts"].(map[string]interface{})["links"] == nil { 717 | t.Fatalf("Posts relationship links were not materialized") 718 | } 719 | if relations["posts"].(map[string]interface{})["meta"] == nil { 720 | t.Fatalf("Posts relationship meta were not materialized") 721 | } 722 | } 723 | 724 | if relations["current_post"] == nil { 725 | t.Fatalf("Current post relationship was not materialized") 726 | } else { 727 | if relations["current_post"].(map[string]interface{})["links"] == nil { 728 | t.Fatalf("Current post relationship links were not materialized") 729 | } 730 | if relations["current_post"].(map[string]interface{})["meta"] == nil { 731 | t.Fatalf("Current post relationship meta were not materialized") 732 | } 733 | } 734 | 735 | if len(relations["posts"].(map[string]interface{})["data"].([]interface{})) != 2 { 736 | t.Fatalf("Did not materialize two posts") 737 | } 738 | } 739 | 740 | func TestNoRelations(t *testing.T) { 741 | testModel := &Blog{ID: 1, Title: "Title 1", CreatedAt: time.Now()} 742 | 743 | out := bytes.NewBuffer(nil) 744 | if err := MarshalPayload(out, testModel); err != nil { 745 | t.Fatal(err) 746 | } 747 | 748 | resp := new(OnePayload) 749 | if err := json.NewDecoder(out).Decode(resp); err != nil { 750 | t.Fatal(err) 751 | } 752 | 753 | if resp.Included != nil { 754 | t.Fatalf("Encoding json response did not omit included") 755 | } 756 | } 757 | 758 | func TestMarshalPayloadWithoutIncluded(t *testing.T) { 759 | data := &Post{ 760 | ID: 1, 761 | BlogID: 2, 762 | ClientID: "123e4567-e89b-12d3-a456-426655440000", 763 | Title: "Foo", 764 | Body: "Bar", 765 | Comments: []*Comment{ 766 | { 767 | ID: 20, 768 | Body: "First", 769 | }, 770 | { 771 | ID: 21, 772 | Body: "Hello World", 773 | }, 774 | }, 775 | LatestComment: &Comment{ 776 | ID: 22, 777 | Body: "Cool!", 778 | }, 779 | } 780 | 781 | out := bytes.NewBuffer(nil) 782 | if err := MarshalPayloadWithoutIncluded(out, data); err != nil { 783 | t.Fatal(err) 784 | } 785 | 786 | resp := new(OnePayload) 787 | if err := json.NewDecoder(out).Decode(resp); err != nil { 788 | t.Fatal(err) 789 | } 790 | 791 | if resp.Included != nil { 792 | t.Fatalf("Encoding json response did not omit included") 793 | } 794 | } 795 | 796 | func TestMarshalPayload_many(t *testing.T) { 797 | data := []interface{}{ 798 | &Blog{ 799 | ID: 5, 800 | Title: "Title 1", 801 | CreatedAt: time.Now(), 802 | Posts: []*Post{ 803 | { 804 | ID: 1, 805 | Title: "Foo", 806 | Body: "Bar", 807 | }, 808 | { 809 | ID: 2, 810 | Title: "Fuubar", 811 | Body: "Bas", 812 | }, 813 | }, 814 | CurrentPost: &Post{ 815 | ID: 1, 816 | Title: "Foo", 817 | Body: "Bar", 818 | }, 819 | }, 820 | &Blog{ 821 | ID: 6, 822 | Title: "Title 2", 823 | CreatedAt: time.Now(), 824 | Posts: []*Post{ 825 | { 826 | ID: 3, 827 | Title: "Foo", 828 | Body: "Bar", 829 | }, 830 | { 831 | ID: 4, 832 | Title: "Fuubar", 833 | Body: "Bas", 834 | }, 835 | }, 836 | CurrentPost: &Post{ 837 | ID: 4, 838 | Title: "Foo", 839 | Body: "Bar", 840 | }, 841 | }, 842 | } 843 | 844 | out := bytes.NewBuffer(nil) 845 | if err := MarshalPayload(out, data); err != nil { 846 | t.Fatal(err) 847 | } 848 | 849 | resp := new(ManyPayload) 850 | if err := json.NewDecoder(out).Decode(resp); err != nil { 851 | t.Fatal(err) 852 | } 853 | 854 | d := resp.Data 855 | 856 | if len(d) != 2 { 857 | t.Fatalf("data should have two elements") 858 | } 859 | } 860 | 861 | func TestMarshalMany_WithSliceOfStructPointers(t *testing.T) { 862 | var data []*Blog 863 | for len(data) < 2 { 864 | data = append(data, testBlog()) 865 | } 866 | 867 | out := bytes.NewBuffer(nil) 868 | if err := MarshalPayload(out, data); err != nil { 869 | t.Fatal(err) 870 | } 871 | 872 | resp := new(ManyPayload) 873 | if err := json.NewDecoder(out).Decode(resp); err != nil { 874 | t.Fatal(err) 875 | } 876 | 877 | d := resp.Data 878 | 879 | if len(d) != 2 { 880 | t.Fatalf("data should have two elements") 881 | } 882 | } 883 | 884 | func TestMarshalManyWithoutIncluded(t *testing.T) { 885 | var data []*Blog 886 | for len(data) < 2 { 887 | data = append(data, testBlog()) 888 | } 889 | 890 | out := bytes.NewBuffer(nil) 891 | if err := MarshalPayloadWithoutIncluded(out, data); err != nil { 892 | t.Fatal(err) 893 | } 894 | 895 | resp := new(ManyPayload) 896 | if err := json.NewDecoder(out).Decode(resp); err != nil { 897 | t.Fatal(err) 898 | } 899 | 900 | d := resp.Data 901 | 902 | if len(d) != 2 { 903 | t.Fatalf("data should have two elements") 904 | } 905 | 906 | if resp.Included != nil { 907 | t.Fatalf("Encoding json response did not omit included") 908 | } 909 | } 910 | 911 | func TestMarshalMany_SliceOfInterfaceAndSliceOfStructsSameJSON(t *testing.T) { 912 | structs := []*Book{ 913 | {ID: 1, Author: "aren55555", ISBN: "abc"}, 914 | {ID: 2, Author: "shwoodard", ISBN: "xyz"}, 915 | } 916 | interfaces := []interface{}{} 917 | for _, s := range structs { 918 | interfaces = append(interfaces, s) 919 | } 920 | 921 | // Perform Marshals 922 | structsOut := new(bytes.Buffer) 923 | if err := MarshalPayload(structsOut, structs); err != nil { 924 | t.Fatal(err) 925 | } 926 | interfacesOut := new(bytes.Buffer) 927 | if err := MarshalPayload(interfacesOut, interfaces); err != nil { 928 | t.Fatal(err) 929 | } 930 | 931 | // Generic JSON Unmarshal 932 | structsData, interfacesData := 933 | make(map[string]interface{}), make(map[string]interface{}) 934 | if err := json.Unmarshal(structsOut.Bytes(), &structsData); err != nil { 935 | t.Fatal(err) 936 | } 937 | if err := json.Unmarshal(interfacesOut.Bytes(), &interfacesData); err != nil { 938 | t.Fatal(err) 939 | } 940 | 941 | // Compare Result 942 | if !reflect.DeepEqual(structsData, interfacesData) { 943 | t.Fatal("Was expecting the JSON API generated to be the same") 944 | } 945 | } 946 | 947 | func TestMarshal_InvalidIntefaceArgument(t *testing.T) { 948 | out := new(bytes.Buffer) 949 | if err := MarshalPayload(out, true); err != ErrUnexpectedType { 950 | t.Fatal("Was expecting an error") 951 | } 952 | if err := MarshalPayload(out, 25); err != ErrUnexpectedType { 953 | t.Fatal("Was expecting an error") 954 | } 955 | if err := MarshalPayload(out, Book{}); err != ErrUnexpectedType { 956 | t.Fatal("Was expecting an error") 957 | } 958 | } 959 | 960 | func testBlog() *Blog { 961 | return &Blog{ 962 | ID: 5, 963 | Title: "Title 1", 964 | CreatedAt: time.Now(), 965 | Posts: []*Post{ 966 | { 967 | ID: 1, 968 | Title: "Foo", 969 | Body: "Bar", 970 | Comments: []*Comment{ 971 | { 972 | ID: 1, 973 | Body: "foo", 974 | }, 975 | { 976 | ID: 2, 977 | Body: "bar", 978 | }, 979 | }, 980 | LatestComment: &Comment{ 981 | ID: 1, 982 | Body: "foo", 983 | }, 984 | }, 985 | { 986 | ID: 2, 987 | Title: "Fuubar", 988 | Body: "Bas", 989 | Comments: []*Comment{ 990 | { 991 | ID: 1, 992 | Body: "foo", 993 | }, 994 | { 995 | ID: 3, 996 | Body: "bas", 997 | }, 998 | }, 999 | LatestComment: &Comment{ 1000 | ID: 1, 1001 | Body: "foo", 1002 | }, 1003 | }, 1004 | }, 1005 | CurrentPost: &Post{ 1006 | ID: 1, 1007 | Title: "Foo", 1008 | Body: "Bar", 1009 | Comments: []*Comment{ 1010 | { 1011 | ID: 1, 1012 | Body: "foo", 1013 | }, 1014 | { 1015 | ID: 2, 1016 | Body: "bar", 1017 | }, 1018 | }, 1019 | LatestComment: &Comment{ 1020 | ID: 1, 1021 | Body: "foo", 1022 | }, 1023 | }, 1024 | } 1025 | } 1026 | -------------------------------------------------------------------------------- /request_test.go: -------------------------------------------------------------------------------- 1 | package jsonapi 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "reflect" 10 | "sort" 11 | "strings" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func TestUnmarshall_attrStringSlice(t *testing.T) { 17 | out := &Book{} 18 | tags := []string{"fiction", "sale"} 19 | data := map[string]interface{}{ 20 | "data": map[string]interface{}{ 21 | "type": "books", 22 | "id": "1", 23 | "attributes": map[string]interface{}{"tags": tags}, 24 | }, 25 | } 26 | b, err := json.Marshal(data) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | if err := UnmarshalPayload(bytes.NewReader(b), out); err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | if e, a := len(tags), len(out.Tags); e != a { 36 | t.Fatalf("Was expecting %d tags, got %d", e, a) 37 | } 38 | 39 | sort.Strings(tags) 40 | sort.Strings(out.Tags) 41 | 42 | for i, tag := range tags { 43 | if e, a := tag, out.Tags[i]; e != a { 44 | t.Fatalf("At index %d, was expecting %s got %s", i, e, a) 45 | } 46 | } 47 | } 48 | 49 | func TestUnmarshalToStructWithPointerAttr(t *testing.T) { 50 | out := new(WithPointer) 51 | in := map[string]interface{}{ 52 | "name": "The name", 53 | "is-active": true, 54 | "int-val": 8, 55 | "float-val": 1.1, 56 | } 57 | if err := UnmarshalPayload(sampleWithPointerPayload(in), out); err != nil { 58 | t.Fatal(err) 59 | } 60 | if *out.Name != "The name" { 61 | t.Fatalf("Error unmarshalling to string ptr") 62 | } 63 | if !*out.IsActive { 64 | t.Fatalf("Error unmarshalling to bool ptr") 65 | } 66 | if *out.IntVal != 8 { 67 | t.Fatalf("Error unmarshalling to int ptr") 68 | } 69 | if *out.FloatVal != 1.1 { 70 | t.Fatalf("Error unmarshalling to float ptr") 71 | } 72 | } 73 | 74 | func TestUnmarshalPayload_missingTypeFieldShouldError(t *testing.T) { 75 | if err := UnmarshalPayload( 76 | strings.NewReader(`{"data":{"body":"hello world"}}`), 77 | &Post{}, 78 | ); err == nil { 79 | t.Fatalf("Expected an error but did not get one") 80 | } 81 | } 82 | 83 | func TestUnmarshalPayload_ptrsAllNil(t *testing.T) { 84 | out := new(WithPointer) 85 | if err := UnmarshalPayload( 86 | strings.NewReader(`{"data":{"type":"with-pointers"}}`), out); err != nil { 87 | t.Fatalf("Error unmarshalling to Foo: %v", err) 88 | } 89 | 90 | if out.ID != nil { 91 | t.Fatalf("Error unmarshalling; expected ID ptr to be nil") 92 | } 93 | } 94 | 95 | func TestUnmarshalPayloadWithPointerID(t *testing.T) { 96 | out := new(WithPointer) 97 | attrs := map[string]interface{}{} 98 | 99 | if err := UnmarshalPayload(sampleWithPointerPayload(attrs), out); err != nil { 100 | t.Fatalf("Error unmarshalling to Foo") 101 | } 102 | 103 | // these were present in the payload -- expect val to be not nil 104 | if out.ID == nil { 105 | t.Fatalf("Error unmarshalling; expected ID ptr to be not nil") 106 | } 107 | if e, a := uint64(2), *out.ID; e != a { 108 | t.Fatalf("Was expecting the ID to have a value of %d, got %d", e, a) 109 | } 110 | } 111 | 112 | func TestUnmarshalPayloadWithPointerAttr_AbsentVal(t *testing.T) { 113 | out := new(WithPointer) 114 | in := map[string]interface{}{ 115 | "name": "The name", 116 | "is-active": true, 117 | } 118 | 119 | if err := UnmarshalPayload(sampleWithPointerPayload(in), out); err != nil { 120 | t.Fatalf("Error unmarshalling to Foo") 121 | } 122 | 123 | // these were present in the payload -- expect val to be not nil 124 | if out.Name == nil || out.IsActive == nil { 125 | t.Fatalf("Error unmarshalling; expected ptr to be not nil") 126 | } 127 | 128 | // these were absent in the payload -- expect val to be nil 129 | if out.IntVal != nil || out.FloatVal != nil { 130 | t.Fatalf("Error unmarshalling; expected ptr to be nil") 131 | } 132 | } 133 | 134 | func TestUnmarshalToStructWithPointerAttr_BadType_bool(t *testing.T) { 135 | out := new(WithPointer) 136 | in := map[string]interface{}{ 137 | "name": true, // This is the wrong type. 138 | } 139 | expectedErrorMessage := "jsonapi: Can't unmarshal true (bool) to struct field `Name`, which is a pointer to `string`" 140 | 141 | err := UnmarshalPayload(sampleWithPointerPayload(in), out) 142 | 143 | if err == nil { 144 | t.Fatalf("Expected error due to invalid type.") 145 | } 146 | if err.Error() != expectedErrorMessage { 147 | t.Fatalf("Unexpected error message: %s", err.Error()) 148 | } 149 | if _, ok := err.(ErrUnsupportedPtrType); !ok { 150 | t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err)) 151 | } 152 | } 153 | 154 | func TestUnmarshalToStructWithPointerAttr_BadType_MapPtr(t *testing.T) { 155 | out := new(WithPointer) 156 | in := map[string]interface{}{ 157 | "name": &map[string]interface{}{"a": 5}, // This is the wrong type. 158 | } 159 | expectedErrorMessage := "jsonapi: Can't unmarshal map[a:5] (map) to struct field `Name`, which is a pointer to `string`" 160 | 161 | err := UnmarshalPayload(sampleWithPointerPayload(in), out) 162 | 163 | if err == nil { 164 | t.Fatalf("Expected error due to invalid type.") 165 | } 166 | if err.Error() != expectedErrorMessage { 167 | t.Fatalf("Unexpected error message: %s", err.Error()) 168 | } 169 | if _, ok := err.(ErrUnsupportedPtrType); !ok { 170 | t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err)) 171 | } 172 | } 173 | 174 | func TestUnmarshalToStructWithPointerAttr_BadType_Struct(t *testing.T) { 175 | out := new(WithPointer) 176 | type FooStruct struct{ A int } 177 | in := map[string]interface{}{ 178 | "name": FooStruct{A: 5}, // This is the wrong type. 179 | } 180 | expectedErrorMessage := "jsonapi: Can't unmarshal map[A:5] (map) to struct field `Name`, which is a pointer to `string`" 181 | 182 | err := UnmarshalPayload(sampleWithPointerPayload(in), out) 183 | 184 | if err == nil { 185 | t.Fatalf("Expected error due to invalid type.") 186 | } 187 | if err.Error() != expectedErrorMessage { 188 | t.Fatalf("Unexpected error message: %s", err.Error()) 189 | } 190 | if _, ok := err.(ErrUnsupportedPtrType); !ok { 191 | t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err)) 192 | } 193 | } 194 | 195 | func TestUnmarshalToStructWithPointerAttr_BadType_IntSlice(t *testing.T) { 196 | out := new(WithPointer) 197 | type FooStruct struct{ A, B int } 198 | in := map[string]interface{}{ 199 | "name": []int{4, 5}, // This is the wrong type. 200 | } 201 | expectedErrorMessage := "jsonapi: Can't unmarshal [4 5] (slice) to struct field `Name`, which is a pointer to `string`" 202 | 203 | err := UnmarshalPayload(sampleWithPointerPayload(in), out) 204 | 205 | if err == nil { 206 | t.Fatalf("Expected error due to invalid type.") 207 | } 208 | if err.Error() != expectedErrorMessage { 209 | t.Fatalf("Unexpected error message: %s", err.Error()) 210 | } 211 | if _, ok := err.(ErrUnsupportedPtrType); !ok { 212 | t.Fatalf("Unexpected error type: %s", reflect.TypeOf(err)) 213 | } 214 | } 215 | 216 | func TestStringPointerField(t *testing.T) { 217 | // Build Book payload 218 | description := "Hello World!" 219 | data := map[string]interface{}{ 220 | "data": map[string]interface{}{ 221 | "type": "books", 222 | "id": "5", 223 | "attributes": map[string]interface{}{ 224 | "author": "aren55555", 225 | "description": description, 226 | "isbn": "", 227 | }, 228 | }, 229 | } 230 | payload, err := json.Marshal(data) 231 | if err != nil { 232 | t.Fatal(err) 233 | } 234 | 235 | // Parse JSON API payload 236 | book := new(Book) 237 | if err := UnmarshalPayload(bytes.NewReader(payload), book); err != nil { 238 | t.Fatal(err) 239 | } 240 | 241 | if book.Description == nil { 242 | t.Fatal("Was not expecting a nil pointer for book.Description") 243 | } 244 | if expected, actual := description, *book.Description; expected != actual { 245 | t.Fatalf("Was expecting descript to be `%s`, got `%s`", expected, actual) 246 | } 247 | } 248 | 249 | func TestMalformedTag(t *testing.T) { 250 | out := new(BadModel) 251 | err := UnmarshalPayload(samplePayload(), out) 252 | if err == nil || err != ErrBadJSONAPIStructTag { 253 | t.Fatalf("Did not error out with wrong number of arguments in tag") 254 | } 255 | } 256 | 257 | func TestUnmarshalInvalidJSON(t *testing.T) { 258 | in := strings.NewReader("{}") 259 | out := new(Blog) 260 | 261 | err := UnmarshalPayload(in, out) 262 | 263 | if err == nil { 264 | t.Fatalf("Did not error out the invalid JSON.") 265 | } 266 | } 267 | 268 | func TestUnmarshalInvalidJSON_BadType(t *testing.T) { 269 | var badTypeTests = []struct { 270 | Field string 271 | BadValue interface{} 272 | Error error 273 | }{ // The `Field` values here correspond to the `ModelBadTypes` jsonapi fields. 274 | {Field: "string_field", BadValue: 0, Error: ErrUnknownFieldNumberType}, // Expected string. 275 | {Field: "float_field", BadValue: "A string.", Error: ErrInvalidType}, // Expected float64. 276 | {Field: "time_field", BadValue: "A string.", Error: ErrInvalidTime}, // Expected int64. 277 | {Field: "time_ptr_field", BadValue: "A string.", Error: ErrInvalidTime}, // Expected *time / int64. 278 | } 279 | for _, test := range badTypeTests { 280 | t.Run(fmt.Sprintf("Test_%s", test.Field), func(t *testing.T) { 281 | out := new(ModelBadTypes) 282 | in := map[string]interface{}{} 283 | in[test.Field] = test.BadValue 284 | expectedErrorMessage := test.Error.Error() 285 | 286 | err := UnmarshalPayload(samplePayloadWithBadTypes(in), out) 287 | 288 | if err == nil { 289 | t.Fatalf("Expected error due to invalid type.") 290 | } 291 | if err.Error() != expectedErrorMessage { 292 | t.Fatalf("Unexpected error message: %s", err.Error()) 293 | } 294 | }) 295 | } 296 | } 297 | 298 | func TestUnmarshalSetsID(t *testing.T) { 299 | in := samplePayloadWithID() 300 | out := new(Blog) 301 | 302 | if err := UnmarshalPayload(in, out); err != nil { 303 | t.Fatal(err) 304 | } 305 | 306 | if out.ID != 2 { 307 | t.Fatalf("Did not set ID on dst interface") 308 | } 309 | } 310 | 311 | func TestUnmarshal_nonNumericID(t *testing.T) { 312 | data := samplePayloadWithoutIncluded() 313 | data["data"].(map[string]interface{})["id"] = "non-numeric-id" 314 | payload, err := json.Marshal(data) 315 | if err != nil { 316 | t.Fatal(err) 317 | } 318 | in := bytes.NewReader(payload) 319 | out := new(Post) 320 | 321 | if err := UnmarshalPayload(in, out); err != ErrBadJSONAPIID { 322 | t.Fatalf( 323 | "Was expecting a `%s` error, got `%s`", 324 | ErrBadJSONAPIID, 325 | err, 326 | ) 327 | } 328 | } 329 | 330 | func TestUnmarshalSetsAttrs(t *testing.T) { 331 | out, err := unmarshalSamplePayload() 332 | if err != nil { 333 | t.Fatal(err) 334 | } 335 | 336 | if out.CreatedAt.IsZero() { 337 | t.Fatalf("Did not parse time") 338 | } 339 | 340 | if out.ViewCount != 1000 { 341 | t.Fatalf("View count not properly serialized") 342 | } 343 | } 344 | 345 | func TestUnmarshal_Times(t *testing.T) { 346 | aTime := time.Date(2016, 8, 17, 8, 27, 12, 0, time.UTC) 347 | 348 | for _, tc := range []struct { 349 | desc string 350 | inputPayload *OnePayload 351 | wantErr bool 352 | verification func(tm *TimestampModel) error 353 | }{ 354 | // Default: 355 | { 356 | desc: "default_byValue", 357 | inputPayload: &OnePayload{ 358 | Data: &Node{ 359 | Type: "timestamps", 360 | Attributes: map[string]interface{}{ 361 | "defaultv": aTime.Unix(), 362 | }, 363 | }, 364 | }, 365 | verification: func(tm *TimestampModel) error { 366 | if !tm.DefaultV.Equal(aTime) { 367 | return errors.New("times not equal!") 368 | } 369 | return nil 370 | }, 371 | }, 372 | { 373 | desc: "default_byPointer", 374 | inputPayload: &OnePayload{ 375 | Data: &Node{ 376 | Type: "timestamps", 377 | Attributes: map[string]interface{}{ 378 | "defaultp": aTime.Unix(), 379 | }, 380 | }, 381 | }, 382 | verification: func(tm *TimestampModel) error { 383 | if !tm.DefaultP.Equal(aTime) { 384 | return errors.New("times not equal!") 385 | } 386 | return nil 387 | }, 388 | }, 389 | { 390 | desc: "default_invalid", 391 | inputPayload: &OnePayload{ 392 | Data: &Node{ 393 | Type: "timestamps", 394 | Attributes: map[string]interface{}{ 395 | "defaultv": "not a timestamp!", 396 | }, 397 | }, 398 | }, 399 | wantErr: true, 400 | }, 401 | // ISO 8601: 402 | { 403 | desc: "iso8601_byValue", 404 | inputPayload: &OnePayload{ 405 | Data: &Node{ 406 | Type: "timestamps", 407 | Attributes: map[string]interface{}{ 408 | "iso8601v": "2016-08-17T08:27:12Z", 409 | }, 410 | }, 411 | }, 412 | verification: func(tm *TimestampModel) error { 413 | if !tm.ISO8601V.Equal(aTime) { 414 | return errors.New("times not equal!") 415 | } 416 | return nil 417 | }, 418 | }, 419 | { 420 | desc: "iso8601_byPointer", 421 | inputPayload: &OnePayload{ 422 | Data: &Node{ 423 | Type: "timestamps", 424 | Attributes: map[string]interface{}{ 425 | "iso8601p": "2016-08-17T08:27:12Z", 426 | }, 427 | }, 428 | }, 429 | verification: func(tm *TimestampModel) error { 430 | if !tm.ISO8601P.Equal(aTime) { 431 | return errors.New("times not equal!") 432 | } 433 | return nil 434 | }, 435 | }, 436 | { 437 | desc: "iso8601_invalid", 438 | inputPayload: &OnePayload{ 439 | Data: &Node{ 440 | Type: "timestamps", 441 | Attributes: map[string]interface{}{ 442 | "iso8601v": "not a timestamp", 443 | }, 444 | }, 445 | }, 446 | wantErr: true, 447 | }, 448 | // RFC 3339 449 | { 450 | desc: "rfc3339_byValue", 451 | inputPayload: &OnePayload{ 452 | Data: &Node{ 453 | Type: "timestamps", 454 | Attributes: map[string]interface{}{ 455 | "rfc3339v": "2016-08-17T08:27:12Z", 456 | }, 457 | }, 458 | }, 459 | verification: func(tm *TimestampModel) error { 460 | if got, want := tm.RFC3339V, aTime; got != want { 461 | return fmt.Errorf("got %v, want %v", got, want) 462 | } 463 | return nil 464 | }, 465 | }, 466 | { 467 | desc: "rfc3339_byPointer", 468 | inputPayload: &OnePayload{ 469 | Data: &Node{ 470 | Type: "timestamps", 471 | Attributes: map[string]interface{}{ 472 | "rfc3339p": "2016-08-17T08:27:12Z", 473 | }, 474 | }, 475 | }, 476 | verification: func(tm *TimestampModel) error { 477 | if got, want := *tm.RFC3339P, aTime; got != want { 478 | return fmt.Errorf("got %v, want %v", got, want) 479 | } 480 | return nil 481 | }, 482 | }, 483 | { 484 | desc: "rfc3339_invalid", 485 | inputPayload: &OnePayload{ 486 | Data: &Node{ 487 | Type: "timestamps", 488 | Attributes: map[string]interface{}{ 489 | "rfc3339v": "not a timestamp", 490 | }, 491 | }, 492 | }, 493 | wantErr: true, 494 | }, 495 | } { 496 | t.Run(tc.desc, func(t *testing.T) { 497 | // Serialize the OnePayload using the standard JSON library. 498 | in := bytes.NewBuffer(nil) 499 | if err := json.NewEncoder(in).Encode(tc.inputPayload); err != nil { 500 | t.Fatal(err) 501 | } 502 | 503 | out := &TimestampModel{} 504 | err := UnmarshalPayload(in, out) 505 | if got, want := (err != nil), tc.wantErr; got != want { 506 | t.Fatalf("UnmarshalPayload error: got %v, want %v", got, want) 507 | } 508 | if tc.verification != nil { 509 | if err := tc.verification(out); err != nil { 510 | t.Fatal(err) 511 | } 512 | } 513 | }) 514 | } 515 | } 516 | 517 | func TestUnmarshalRelationshipsWithoutIncluded(t *testing.T) { 518 | data, err := json.Marshal(samplePayloadWithoutIncluded()) 519 | if err != nil { 520 | t.Fatal(err) 521 | } 522 | in := bytes.NewReader(data) 523 | out := new(Post) 524 | 525 | if err := UnmarshalPayload(in, out); err != nil { 526 | t.Fatal(err) 527 | } 528 | 529 | // Verify each comment has at least an ID 530 | for _, comment := range out.Comments { 531 | if comment.ID == 0 { 532 | t.Fatalf("The comment did not have an ID") 533 | } 534 | } 535 | } 536 | 537 | func TestUnmarshalRelationships(t *testing.T) { 538 | out, err := unmarshalSamplePayload() 539 | if err != nil { 540 | t.Fatal(err) 541 | } 542 | 543 | if out.CurrentPost == nil { 544 | t.Fatalf("Current post was not materialized") 545 | } 546 | 547 | if out.CurrentPost.Title != "Bas" || out.CurrentPost.Body != "Fuubar" { 548 | t.Fatalf("Attributes were not set") 549 | } 550 | 551 | if len(out.Posts) != 2 { 552 | t.Fatalf("There should have been 2 posts") 553 | } 554 | } 555 | 556 | func TestUnmarshalNullRelationship(t *testing.T) { 557 | sample := map[string]interface{}{ 558 | "data": map[string]interface{}{ 559 | "type": "posts", 560 | "id": "1", 561 | "attributes": map[string]interface{}{ 562 | "body": "Hello", 563 | "title": "World", 564 | }, 565 | "relationships": map[string]interface{}{ 566 | "latest_comment": map[string]interface{}{ 567 | "data": nil, // empty to-one relationship 568 | }, 569 | }, 570 | }, 571 | } 572 | data, err := json.Marshal(sample) 573 | if err != nil { 574 | t.Fatal(err) 575 | } 576 | 577 | in := bytes.NewReader(data) 578 | out := new(Post) 579 | 580 | if err := UnmarshalPayload(in, out); err != nil { 581 | t.Fatal(err) 582 | } 583 | 584 | if out.LatestComment != nil { 585 | t.Fatalf("Latest Comment was not set to nil") 586 | } 587 | } 588 | 589 | func TestUnmarshalNullRelationshipInSlice(t *testing.T) { 590 | sample := map[string]interface{}{ 591 | "data": map[string]interface{}{ 592 | "type": "posts", 593 | "id": "1", 594 | "attributes": map[string]interface{}{ 595 | "body": "Hello", 596 | "title": "World", 597 | }, 598 | "relationships": map[string]interface{}{ 599 | "comments": map[string]interface{}{ 600 | "data": []interface{}{}, // empty to-many relationships 601 | }, 602 | }, 603 | }, 604 | } 605 | data, err := json.Marshal(sample) 606 | if err != nil { 607 | t.Fatal(err) 608 | } 609 | 610 | in := bytes.NewReader(data) 611 | out := new(Post) 612 | 613 | if err := UnmarshalPayload(in, out); err != nil { 614 | t.Fatal(err) 615 | } 616 | 617 | if len(out.Comments) != 0 { 618 | t.Fatalf("Wrong number of comments; Comments should be empty") 619 | } 620 | } 621 | 622 | func TestUnmarshalNestedRelationships(t *testing.T) { 623 | out, err := unmarshalSamplePayload() 624 | if err != nil { 625 | t.Fatal(err) 626 | } 627 | 628 | if out.CurrentPost == nil { 629 | t.Fatalf("Current post was not materialized") 630 | } 631 | 632 | if out.CurrentPost.Comments == nil { 633 | t.Fatalf("Did not materialize nested records, comments") 634 | } 635 | 636 | if len(out.CurrentPost.Comments) != 2 { 637 | t.Fatalf("Wrong number of comments") 638 | } 639 | } 640 | 641 | func TestUnmarshalRelationshipsSerializedEmbedded(t *testing.T) { 642 | out := sampleSerializedEmbeddedTestModel() 643 | 644 | if out.CurrentPost == nil { 645 | t.Fatalf("Current post was not materialized") 646 | } 647 | 648 | if out.CurrentPost.Title != "Foo" || out.CurrentPost.Body != "Bar" { 649 | t.Fatalf("Attributes were not set") 650 | } 651 | 652 | if len(out.Posts) != 2 { 653 | t.Fatalf("There should have been 2 posts") 654 | } 655 | 656 | if out.Posts[0].LatestComment.Body != "foo" { 657 | t.Fatalf("The comment body was not set") 658 | } 659 | } 660 | 661 | func TestUnmarshalNestedRelationshipsEmbedded(t *testing.T) { 662 | out := bytes.NewBuffer(nil) 663 | if err := MarshalOnePayloadEmbedded(out, testModel()); err != nil { 664 | t.Fatal(err) 665 | } 666 | 667 | model := new(Blog) 668 | 669 | if err := UnmarshalPayload(out, model); err != nil { 670 | t.Fatal(err) 671 | } 672 | 673 | if model.CurrentPost == nil { 674 | t.Fatalf("Current post was not materialized") 675 | } 676 | 677 | if model.CurrentPost.Comments == nil { 678 | t.Fatalf("Did not materialize nested records, comments") 679 | } 680 | 681 | if len(model.CurrentPost.Comments) != 2 { 682 | t.Fatalf("Wrong number of comments") 683 | } 684 | 685 | if model.CurrentPost.Comments[0].Body != "foo" { 686 | t.Fatalf("Comment body not set") 687 | } 688 | } 689 | 690 | func TestUnmarshalRelationshipsSideloaded(t *testing.T) { 691 | payload := samplePayloadWithSideloaded() 692 | out := new(Blog) 693 | 694 | if err := UnmarshalPayload(payload, out); err != nil { 695 | t.Fatal(err) 696 | } 697 | 698 | if out.CurrentPost == nil { 699 | t.Fatalf("Current post was not materialized") 700 | } 701 | 702 | if out.CurrentPost.Title != "Foo" || out.CurrentPost.Body != "Bar" { 703 | t.Fatalf("Attributes were not set") 704 | } 705 | 706 | if len(out.Posts) != 2 { 707 | t.Fatalf("There should have been 2 posts") 708 | } 709 | } 710 | 711 | func TestUnmarshalNestedRelationshipsSideloaded(t *testing.T) { 712 | payload := samplePayloadWithSideloaded() 713 | out := new(Blog) 714 | 715 | if err := UnmarshalPayload(payload, out); err != nil { 716 | t.Fatal(err) 717 | } 718 | 719 | if out.CurrentPost == nil { 720 | t.Fatalf("Current post was not materialized") 721 | } 722 | 723 | if out.CurrentPost.Comments == nil { 724 | t.Fatalf("Did not materialize nested records, comments") 725 | } 726 | 727 | if len(out.CurrentPost.Comments) != 2 { 728 | t.Fatalf("Wrong number of comments") 729 | } 730 | 731 | if out.CurrentPost.Comments[0].Body != "foo" { 732 | t.Fatalf("Comment body not set") 733 | } 734 | } 735 | 736 | func TestUnmarshalNestedRelationshipsEmbedded_withClientIDs(t *testing.T) { 737 | model := new(Blog) 738 | 739 | if err := UnmarshalPayload(samplePayload(), model); err != nil { 740 | t.Fatal(err) 741 | } 742 | 743 | if model.Posts[0].ClientID == "" { 744 | t.Fatalf("ClientID not set from request on related record") 745 | } 746 | } 747 | 748 | func unmarshalSamplePayload() (*Blog, error) { 749 | in := samplePayload() 750 | out := new(Blog) 751 | 752 | if err := UnmarshalPayload(in, out); err != nil { 753 | return nil, err 754 | } 755 | 756 | return out, nil 757 | } 758 | 759 | func TestUnmarshalManyPayload(t *testing.T) { 760 | sample := map[string]interface{}{ 761 | "data": []interface{}{ 762 | map[string]interface{}{ 763 | "type": "posts", 764 | "id": "1", 765 | "attributes": map[string]interface{}{ 766 | "body": "First", 767 | "title": "Post", 768 | }, 769 | }, 770 | map[string]interface{}{ 771 | "type": "posts", 772 | "id": "2", 773 | "attributes": map[string]interface{}{ 774 | "body": "Second", 775 | "title": "Post", 776 | }, 777 | }, 778 | }, 779 | } 780 | 781 | data, err := json.Marshal(sample) 782 | if err != nil { 783 | t.Fatal(err) 784 | } 785 | in := bytes.NewReader(data) 786 | 787 | posts, err := UnmarshalManyPayload(in, reflect.TypeOf(new(Post))) 788 | if err != nil { 789 | t.Fatal(err) 790 | } 791 | 792 | if len(posts) != 2 { 793 | t.Fatal("Wrong number of posts") 794 | } 795 | 796 | for _, p := range posts { 797 | _, ok := p.(*Post) 798 | if !ok { 799 | t.Fatal("Was expecting a Post") 800 | } 801 | } 802 | } 803 | 804 | func TestManyPayload_withLinks(t *testing.T) { 805 | firstPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=50" 806 | prevPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=0" 807 | nextPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=100" 808 | lastPageURL := "http://somesite.com/movies?page[limit]=50&page[offset]=500" 809 | 810 | sample := map[string]interface{}{ 811 | "data": []interface{}{ 812 | map[string]interface{}{ 813 | "type": "posts", 814 | "id": "1", 815 | "attributes": map[string]interface{}{ 816 | "body": "First", 817 | "title": "Post", 818 | }, 819 | }, 820 | map[string]interface{}{ 821 | "type": "posts", 822 | "id": "2", 823 | "attributes": map[string]interface{}{ 824 | "body": "Second", 825 | "title": "Post", 826 | }, 827 | }, 828 | }, 829 | "links": map[string]interface{}{ 830 | KeyFirstPage: firstPageURL, 831 | KeyPreviousPage: prevPageURL, 832 | KeyNextPage: nextPageURL, 833 | KeyLastPage: lastPageURL, 834 | }, 835 | } 836 | 837 | data, err := json.Marshal(sample) 838 | if err != nil { 839 | t.Fatal(err) 840 | } 841 | in := bytes.NewReader(data) 842 | 843 | payload := new(ManyPayload) 844 | if err = json.NewDecoder(in).Decode(payload); err != nil { 845 | t.Fatal(err) 846 | } 847 | 848 | if payload.Links == nil { 849 | t.Fatal("Was expecting a non nil ptr Link field") 850 | } 851 | 852 | links := *payload.Links 853 | 854 | first, ok := links[KeyFirstPage] 855 | if !ok { 856 | t.Fatal("Was expecting a non nil ptr Link field") 857 | } 858 | if e, a := firstPageURL, first; e != a { 859 | t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyFirstPage, e, a) 860 | } 861 | 862 | prev, ok := links[KeyPreviousPage] 863 | if !ok { 864 | t.Fatal("Was expecting a non nil ptr Link field") 865 | } 866 | if e, a := prevPageURL, prev; e != a { 867 | t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyPreviousPage, e, a) 868 | } 869 | 870 | next, ok := links[KeyNextPage] 871 | if !ok { 872 | t.Fatal("Was expecting a non nil ptr Link field") 873 | } 874 | if e, a := nextPageURL, next; e != a { 875 | t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyNextPage, e, a) 876 | } 877 | 878 | last, ok := links[KeyLastPage] 879 | if !ok { 880 | t.Fatal("Was expecting a non nil ptr Link field") 881 | } 882 | if e, a := lastPageURL, last; e != a { 883 | t.Fatalf("Was expecting links.%s to have a value of %s, got %s", KeyLastPage, e, a) 884 | } 885 | } 886 | 887 | func TestUnmarshalCustomTypeAttributes(t *testing.T) { 888 | customInt := CustomIntType(5) 889 | customFloat := CustomFloatType(1.5) 890 | customString := CustomStringType("Test") 891 | 892 | data := map[string]interface{}{ 893 | "data": map[string]interface{}{ 894 | "type": "customtypes", 895 | "id": "1", 896 | "attributes": map[string]interface{}{ 897 | "int": 5, 898 | "intptr": 5, 899 | "intptrnull": nil, 900 | 901 | "float": 1.5, 902 | "string": "Test", 903 | }, 904 | }, 905 | } 906 | payload, err := json.Marshal(data) 907 | if err != nil { 908 | t.Fatal(err) 909 | } 910 | 911 | // Parse JSON API payload 912 | customAttributeTypes := new(CustomAttributeTypes) 913 | if err := UnmarshalPayload(bytes.NewReader(payload), customAttributeTypes); err != nil { 914 | t.Fatal(err) 915 | } 916 | 917 | if expected, actual := customInt, customAttributeTypes.Int; expected != actual { 918 | t.Fatalf("Was expecting custom int to be `%d`, got `%d`", expected, actual) 919 | } 920 | if expected, actual := customInt, *customAttributeTypes.IntPtr; expected != actual { 921 | t.Fatalf("Was expecting custom int pointer to be `%d`, got `%d`", expected, actual) 922 | } 923 | if customAttributeTypes.IntPtrNull != nil { 924 | t.Fatalf("Was expecting custom int pointer to be , got `%d`", customAttributeTypes.IntPtrNull) 925 | } 926 | 927 | if expected, actual := customFloat, customAttributeTypes.Float; expected != actual { 928 | t.Fatalf("Was expecting custom float to be `%f`, got `%f`", expected, actual) 929 | } 930 | if expected, actual := customString, customAttributeTypes.String; expected != actual { 931 | t.Fatalf("Was expecting custom string to be `%s`, got `%s`", expected, actual) 932 | } 933 | } 934 | 935 | func TestUnmarshalCustomTypeAttributes_ErrInvalidType(t *testing.T) { 936 | data := map[string]interface{}{ 937 | "data": map[string]interface{}{ 938 | "type": "customtypes", 939 | "id": "1", 940 | "attributes": map[string]interface{}{ 941 | "int": "bad", 942 | "intptr": 5, 943 | "intptrnull": nil, 944 | 945 | "float": 1.5, 946 | "string": "Test", 947 | }, 948 | }, 949 | } 950 | payload, err := json.Marshal(data) 951 | if err != nil { 952 | t.Fatal(err) 953 | } 954 | 955 | // Parse JSON API payload 956 | customAttributeTypes := new(CustomAttributeTypes) 957 | err = UnmarshalPayload(bytes.NewReader(payload), customAttributeTypes) 958 | if err == nil { 959 | t.Fatal("Expected an error unmarshalling the payload due to type mismatch, got none") 960 | } 961 | 962 | if err != ErrInvalidType { 963 | t.Fatalf("Expected error to be %v, was %v", ErrInvalidType, err) 964 | } 965 | } 966 | 967 | func samplePayloadWithoutIncluded() map[string]interface{} { 968 | return map[string]interface{}{ 969 | "data": map[string]interface{}{ 970 | "type": "posts", 971 | "id": "1", 972 | "attributes": map[string]interface{}{ 973 | "body": "Hello", 974 | "title": "World", 975 | }, 976 | "relationships": map[string]interface{}{ 977 | "comments": map[string]interface{}{ 978 | "data": []interface{}{ 979 | map[string]interface{}{ 980 | "type": "comments", 981 | "id": "123", 982 | }, 983 | map[string]interface{}{ 984 | "type": "comments", 985 | "id": "456", 986 | }, 987 | }, 988 | }, 989 | "latest_comment": map[string]interface{}{ 990 | "data": map[string]interface{}{ 991 | "type": "comments", 992 | "id": "55555", 993 | }, 994 | }, 995 | }, 996 | }, 997 | } 998 | } 999 | 1000 | func samplePayload() io.Reader { 1001 | payload := &OnePayload{ 1002 | Data: &Node{ 1003 | Type: "blogs", 1004 | Attributes: map[string]interface{}{ 1005 | "title": "New blog", 1006 | "created_at": 1436216820, 1007 | "view_count": 1000, 1008 | }, 1009 | Relationships: map[string]interface{}{ 1010 | "posts": &RelationshipManyNode{ 1011 | Data: []*Node{ 1012 | { 1013 | Type: "posts", 1014 | Attributes: map[string]interface{}{ 1015 | "title": "Foo", 1016 | "body": "Bar", 1017 | }, 1018 | ClientID: "1", 1019 | }, 1020 | { 1021 | Type: "posts", 1022 | Attributes: map[string]interface{}{ 1023 | "title": "X", 1024 | "body": "Y", 1025 | }, 1026 | ClientID: "2", 1027 | }, 1028 | }, 1029 | }, 1030 | "current_post": &RelationshipOneNode{ 1031 | Data: &Node{ 1032 | Type: "posts", 1033 | Attributes: map[string]interface{}{ 1034 | "title": "Bas", 1035 | "body": "Fuubar", 1036 | }, 1037 | ClientID: "3", 1038 | Relationships: map[string]interface{}{ 1039 | "comments": &RelationshipManyNode{ 1040 | Data: []*Node{ 1041 | { 1042 | Type: "comments", 1043 | Attributes: map[string]interface{}{ 1044 | "body": "Great post!", 1045 | }, 1046 | ClientID: "4", 1047 | }, 1048 | { 1049 | Type: "comments", 1050 | Attributes: map[string]interface{}{ 1051 | "body": "Needs some work!", 1052 | }, 1053 | ClientID: "5", 1054 | }, 1055 | }, 1056 | }, 1057 | }, 1058 | }, 1059 | }, 1060 | }, 1061 | }, 1062 | } 1063 | 1064 | out := bytes.NewBuffer(nil) 1065 | json.NewEncoder(out).Encode(payload) 1066 | 1067 | return out 1068 | } 1069 | 1070 | func samplePayloadWithID() io.Reader { 1071 | payload := &OnePayload{ 1072 | Data: &Node{ 1073 | ID: "2", 1074 | Type: "blogs", 1075 | Attributes: map[string]interface{}{ 1076 | "title": "New blog", 1077 | "view_count": 1000, 1078 | }, 1079 | }, 1080 | } 1081 | 1082 | out := bytes.NewBuffer(nil) 1083 | json.NewEncoder(out).Encode(payload) 1084 | 1085 | return out 1086 | } 1087 | 1088 | func samplePayloadWithBadTypes(m map[string]interface{}) io.Reader { 1089 | payload := &OnePayload{ 1090 | Data: &Node{ 1091 | ID: "2", 1092 | Type: "badtypes", 1093 | Attributes: m, 1094 | }, 1095 | } 1096 | 1097 | out := bytes.NewBuffer(nil) 1098 | json.NewEncoder(out).Encode(payload) 1099 | 1100 | return out 1101 | } 1102 | 1103 | func sampleWithPointerPayload(m map[string]interface{}) io.Reader { 1104 | payload := &OnePayload{ 1105 | Data: &Node{ 1106 | ID: "2", 1107 | Type: "with-pointers", 1108 | Attributes: m, 1109 | }, 1110 | } 1111 | 1112 | out := bytes.NewBuffer(nil) 1113 | json.NewEncoder(out).Encode(payload) 1114 | 1115 | return out 1116 | } 1117 | 1118 | func testModel() *Blog { 1119 | return &Blog{ 1120 | ID: 5, 1121 | ClientID: "1", 1122 | Title: "Title 1", 1123 | CreatedAt: time.Now(), 1124 | Posts: []*Post{ 1125 | { 1126 | ID: 1, 1127 | Title: "Foo", 1128 | Body: "Bar", 1129 | Comments: []*Comment{ 1130 | { 1131 | ID: 1, 1132 | Body: "foo", 1133 | }, 1134 | { 1135 | ID: 2, 1136 | Body: "bar", 1137 | }, 1138 | }, 1139 | LatestComment: &Comment{ 1140 | ID: 1, 1141 | Body: "foo", 1142 | }, 1143 | }, 1144 | { 1145 | ID: 2, 1146 | Title: "Fuubar", 1147 | Body: "Bas", 1148 | Comments: []*Comment{ 1149 | { 1150 | ID: 1, 1151 | Body: "foo", 1152 | }, 1153 | { 1154 | ID: 3, 1155 | Body: "bas", 1156 | }, 1157 | }, 1158 | LatestComment: &Comment{ 1159 | ID: 1, 1160 | Body: "foo", 1161 | }, 1162 | }, 1163 | }, 1164 | CurrentPost: &Post{ 1165 | ID: 1, 1166 | Title: "Foo", 1167 | Body: "Bar", 1168 | Comments: []*Comment{ 1169 | { 1170 | ID: 1, 1171 | Body: "foo", 1172 | }, 1173 | { 1174 | ID: 2, 1175 | Body: "bar", 1176 | }, 1177 | }, 1178 | LatestComment: &Comment{ 1179 | ID: 1, 1180 | Body: "foo", 1181 | }, 1182 | }, 1183 | } 1184 | } 1185 | 1186 | func samplePayloadWithSideloaded() io.Reader { 1187 | testModel := testModel() 1188 | 1189 | out := bytes.NewBuffer(nil) 1190 | MarshalPayload(out, testModel) 1191 | 1192 | return out 1193 | } 1194 | 1195 | func sampleSerializedEmbeddedTestModel() *Blog { 1196 | out := bytes.NewBuffer(nil) 1197 | MarshalOnePayloadEmbedded(out, testModel()) 1198 | 1199 | blog := new(Blog) 1200 | UnmarshalPayload(out, blog) 1201 | 1202 | return blog 1203 | } 1204 | 1205 | func TestUnmarshalNestedStructPtr(t *testing.T) { 1206 | type Director struct { 1207 | Firstname string `jsonapi:"attr,firstname"` 1208 | Surname string `jsonapi:"attr,surname"` 1209 | } 1210 | type Movie struct { 1211 | ID string `jsonapi:"primary,movies"` 1212 | Name string `jsonapi:"attr,name"` 1213 | Director *Director `jsonapi:"attr,director"` 1214 | } 1215 | sample := map[string]interface{}{ 1216 | "data": map[string]interface{}{ 1217 | "type": "movies", 1218 | "id": "123", 1219 | "attributes": map[string]interface{}{ 1220 | "name": "The Shawshank Redemption", 1221 | "director": map[string]interface{}{ 1222 | "firstname": "Frank", 1223 | "surname": "Darabont", 1224 | }, 1225 | }, 1226 | }, 1227 | } 1228 | 1229 | data, err := json.Marshal(sample) 1230 | if err != nil { 1231 | t.Fatal(err) 1232 | } 1233 | in := bytes.NewReader(data) 1234 | out := new(Movie) 1235 | 1236 | if err := UnmarshalPayload(in, out); err != nil { 1237 | t.Fatal(err) 1238 | } 1239 | 1240 | if out.Name != "The Shawshank Redemption" { 1241 | t.Fatalf("expected out.Name to be `The Shawshank Redemption`, but got `%s`", out.Name) 1242 | } 1243 | if out.Director.Firstname != "Frank" { 1244 | t.Fatalf("expected out.Director.Firstname to be `Frank`, but got `%s`", out.Director.Firstname) 1245 | } 1246 | if out.Director.Surname != "Darabont" { 1247 | t.Fatalf("expected out.Director.Surname to be `Darabont`, but got `%s`", out.Director.Surname) 1248 | } 1249 | } 1250 | 1251 | func TestUnmarshalNestedStruct(t *testing.T) { 1252 | boss := map[string]interface{}{ 1253 | "firstname": "Hubert", 1254 | "surname": "Farnsworth", 1255 | "age": 176, 1256 | "hired-at": "2016-08-17T08:27:12Z", 1257 | } 1258 | 1259 | sample := map[string]interface{}{ 1260 | "data": map[string]interface{}{ 1261 | "type": "companies", 1262 | "id": "123", 1263 | "attributes": map[string]interface{}{ 1264 | "name": "Planet Express", 1265 | "boss": boss, 1266 | "founded-at": "2016-08-17T08:27:12Z", 1267 | "teams": []map[string]interface{}{ 1268 | map[string]interface{}{ 1269 | "name": "Dev", 1270 | "members": []map[string]interface{}{ 1271 | map[string]interface{}{"firstname": "Sean"}, 1272 | map[string]interface{}{"firstname": "Iz"}, 1273 | }, 1274 | "leader": map[string]interface{}{"firstname": "Iz"}, 1275 | }, 1276 | map[string]interface{}{ 1277 | "name": "DxE", 1278 | "members": []map[string]interface{}{ 1279 | map[string]interface{}{"firstname": "Akshay"}, 1280 | map[string]interface{}{"firstname": "Peri"}, 1281 | }, 1282 | "leader": map[string]interface{}{"firstname": "Peri"}, 1283 | }, 1284 | }, 1285 | }, 1286 | }, 1287 | } 1288 | 1289 | data, err := json.Marshal(sample) 1290 | if err != nil { 1291 | t.Fatal(err) 1292 | } 1293 | in := bytes.NewReader(data) 1294 | out := new(Company) 1295 | 1296 | if err := UnmarshalPayload(in, out); err != nil { 1297 | t.Fatal(err) 1298 | } 1299 | 1300 | if out.Boss.Firstname != "Hubert" { 1301 | t.Fatalf("expected `Hubert` at out.Boss.Firstname, but got `%s`", out.Boss.Firstname) 1302 | } 1303 | 1304 | if out.Boss.Age != 176 { 1305 | t.Fatalf("expected `176` at out.Boss.Age, but got `%d`", out.Boss.Age) 1306 | } 1307 | 1308 | if out.Boss.HiredAt.IsZero() { 1309 | t.Fatalf("expected out.Boss.HiredAt to be zero, but got `%t`", out.Boss.HiredAt.IsZero()) 1310 | } 1311 | 1312 | if len(out.Teams) != 2 { 1313 | t.Fatalf("expected len(out.Teams) to be 2, but got `%d`", len(out.Teams)) 1314 | } 1315 | 1316 | if out.Teams[0].Name != "Dev" { 1317 | t.Fatalf("expected out.Teams[0].Name to be `Dev`, but got `%s`", out.Teams[0].Name) 1318 | } 1319 | 1320 | if out.Teams[1].Name != "DxE" { 1321 | t.Fatalf("expected out.Teams[1].Name to be `DxE`, but got `%s`", out.Teams[1].Name) 1322 | } 1323 | 1324 | if len(out.Teams[0].Members) != 2 { 1325 | t.Fatalf("expected len(out.Teams[0].Members) to be 2, but got `%d`", len(out.Teams[0].Members)) 1326 | } 1327 | 1328 | if len(out.Teams[1].Members) != 2 { 1329 | t.Fatalf("expected len(out.Teams[1].Members) to be 2, but got `%d`", len(out.Teams[1].Members)) 1330 | } 1331 | 1332 | if out.Teams[0].Members[0].Firstname != "Sean" { 1333 | t.Fatalf("expected out.Teams[0].Members[0].Firstname to be `Sean`, but got `%s`", out.Teams[0].Members[0].Firstname) 1334 | } 1335 | 1336 | if out.Teams[0].Members[1].Firstname != "Iz" { 1337 | t.Fatalf("expected out.Teams[0].Members[1].Firstname to be `Iz`, but got `%s`", out.Teams[0].Members[1].Firstname) 1338 | } 1339 | 1340 | if out.Teams[1].Members[0].Firstname != "Akshay" { 1341 | t.Fatalf("expected out.Teams[1].Members[0].Firstname to be `Akshay`, but got `%s`", out.Teams[1].Members[0].Firstname) 1342 | } 1343 | 1344 | if out.Teams[1].Members[1].Firstname != "Peri" { 1345 | t.Fatalf("expected out.Teams[1].Members[1].Firstname to be `Peri`, but got `%s`", out.Teams[1].Members[1].Firstname) 1346 | } 1347 | 1348 | if out.Teams[0].Leader.Firstname != "Iz" { 1349 | t.Fatalf("expected out.Teams[0].Leader.Firstname to be `Iz`, but got `%s`", out.Teams[0].Leader.Firstname) 1350 | } 1351 | 1352 | if out.Teams[1].Leader.Firstname != "Peri" { 1353 | t.Fatalf("expected out.Teams[1].Leader.Firstname to be `Peri`, but got `%s`", out.Teams[1].Leader.Firstname) 1354 | } 1355 | } 1356 | 1357 | func TestUnmarshalNestedStructSlice(t *testing.T) { 1358 | 1359 | fry := map[string]interface{}{ 1360 | "firstname": "Philip J.", 1361 | "surname": "Fry", 1362 | "age": 25, 1363 | "hired-at": "2016-08-17T08:27:12Z", 1364 | } 1365 | 1366 | bender := map[string]interface{}{ 1367 | "firstname": "Bender Bending", 1368 | "surname": "Rodriguez", 1369 | "age": 19, 1370 | "hired-at": "2016-08-17T08:27:12Z", 1371 | } 1372 | 1373 | deliveryCrew := map[string]interface{}{ 1374 | "name": "Delivery Crew", 1375 | "members": []interface{}{ 1376 | fry, 1377 | bender, 1378 | }, 1379 | } 1380 | 1381 | sample := map[string]interface{}{ 1382 | "data": map[string]interface{}{ 1383 | "type": "companies", 1384 | "id": "123", 1385 | "attributes": map[string]interface{}{ 1386 | "name": "Planet Express", 1387 | "teams": []interface{}{ 1388 | deliveryCrew, 1389 | }, 1390 | }, 1391 | }, 1392 | } 1393 | 1394 | data, err := json.Marshal(sample) 1395 | if err != nil { 1396 | t.Fatal(err) 1397 | } 1398 | in := bytes.NewReader(data) 1399 | out := new(Company) 1400 | 1401 | if err := UnmarshalPayload(in, out); err != nil { 1402 | t.Fatal(err) 1403 | } 1404 | 1405 | if out.Teams[0].Name != "Delivery Crew" { 1406 | t.Fatalf("Nested struct not unmarshalled: Expected `Delivery Crew` but got `%s`", out.Teams[0].Name) 1407 | } 1408 | 1409 | if len(out.Teams[0].Members) != 2 { 1410 | t.Fatalf("Nested struct not unmarshalled: Expected to have `2` Members but got `%d`", 1411 | len(out.Teams[0].Members)) 1412 | } 1413 | 1414 | if out.Teams[0].Members[0].Firstname != "Philip J." { 1415 | t.Fatalf("Nested struct not unmarshalled: Expected `Philip J.` but got `%s`", 1416 | out.Teams[0].Members[0].Firstname) 1417 | } 1418 | } 1419 | --------------------------------------------------------------------------------