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 8A 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``` 16go get -u github.com/google/jsonapi 17``` 18 19Or, see [Alternative Installation](#alternative-installation). 20 21## Background 22 23You are working in your Go web application and you have a struct that is 24organized similarly to your database schema. You need to send and 25receive json payloads that adhere to the JSON API spec. Once you realize that 26your json needed to take on this special form, you go down the path of 27creating more structs to be able to serialize and deserialize JSON API 28payloads. Then there are more models required with this additional 29structure. Ugh! With JSON API, you can keep your model structs as is and 30use [StructTags](http://golang.org/pkg/reflect/#StructTag) to indicate 31to JSON API how you want your response built or your request 32deserialized. What about your relationships? JSON API supports 33relationships out of the box and will even put them in your response 34into an `included` side-loaded slice--that contains associated records. 35 36## Introduction 37 38JSON API uses [StructField](http://golang.org/pkg/reflect/#StructField) 39tags to annotate the structs fields that you already have and use in 40your app and then reads and writes [JSON API](http://jsonapi.org) 41output based on the instructions you give the library in your JSON API 42tags. Let's take an example. In your app, you most likely have structs 43that look similar to these: 44 45 46```go 47type 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 57type 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 65type 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 73These structs may or may not resemble the layout of your database. But 74these are the ones that you want to use right? You wouldn't want to use 75structs like those that JSON API sends because it is difficult to get at 76all of your data easily. 77 78## Example App 79 80[examples/app.go](https://github.com/google/jsonapi/blob/master/examples/app.go) 81 82This program demonstrates the implementation of a create, a show, 83and a list [http.Handler](http://golang.org/pkg/net/http#Handler). It 84outputs some example requests and responses as well as serialized 85examples of the source/target structs to json. That is to say, I show 86you that the library has successfully taken your JSON API request and 87turned it into your struct types. 88 89To 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 103The `jsonapi` [StructTags](http://golang.org/pkg/reflect/#StructTag) 104tells this library how to marshal and unmarshal your structs into 105JSON API payloads and your JSON API payloads to structs, respectively. 106Then Use JSON API's Marshal and Unmarshal methods to construct and read 107your responses and replies. Here's an example of the structs above 108using JSON API tags: 109 110```go 111type 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 121type 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 129type 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,<type field output>"` 143``` 144 145This indicates this is the primary key field for this struct type. 146Tag 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 151types are shown in the examples, but not required. 152 153#### `attr` 154 155``` 156`jsonapi:"attr,<key name in attributes hash>,<optional: omitempty>"` 157``` 158 159These fields' values will end up in the `attributes`hash for a record. 160The first argument must be, `attr`, and the second should be the name 161for the key to display in the `attributes` hash for that record. The optional 162third argument is `omitempty` - if it is present the field will not be present 163in the `"attributes"` if the field's value is equivalent to the field types 164empty value (ie if the `count` field is of type `int`, `omitempty` will omit the 165field 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,<key name in relationships hash>,<optional: omitempty>"` 172``` 173 174Relations are struct fields that represent a one-to-one or one-to-many 175relationship with other structs. JSON API will traverse the graph of 176relationships and marshal or unmarshal records. The first argument must 177be, `relation`, and the second should be the name of the relationship, 178used as the key in the `relationships` hash for the record. The optional 179third argument is `omitempty` - if present will prevent non existent to-one and 180to-many from being serialized. 181 182#### `links` 183 184*Note: This annotation is an added feature independent of the canonical google/jsonapi package* 185 186``` 187`jsonapi:"links,omitempty"` 188``` 189 190A field annotated with `links` will have the links members of the request unmarshaled to it. Note 191that this field should _always_ be annotated with `omitempty`, as marshaling of links members is 192instead handled by the `Linkable` interface (see `Links` below). 193 194## Methods Reference 195 196**All `Marshal` and `Unmarshal` methods expect pointers to struct 197instance or slices of the same contained with the `interface{}`s** 198 199Now you have your structs prepared to be serialized or materialized, What 200about the rest? 201 202### Create Record Example 203 204You can Unmarshal a JSON API payload using 205[jsonapi.UnmarshalPayload](http://godoc.org/github.com/google/jsonapi#UnmarshalPayload). 206It reads from an [io.Reader](https://golang.org/pkg/io/#Reader) 207containing a JSON API payload for one record (but can have related 208records). Then, it materializes a struct that you created and passed in 209(using new or &). Again, the method supports single records only, at 210the top level, in request payloads at the moment. Bulk creates and 211updates are not supported yet. 212 213After saving your record, you can use, 214[MarshalOnePayload](http://godoc.org/github.com/google/jsonapi#MarshalOnePayload), 215to write the JSON API response to an 216[io.Writer](https://golang.org/pkg/io/#Writer). 217 218#### `UnmarshalPayload` 219 220```go 221UnmarshalPayload(in io.Reader, model interface{}) 222``` 223 224Visit [godoc](http://godoc.org/github.com/google/jsonapi#UnmarshalPayload) 225 226#### `MarshalPayload` 227 228```go 229MarshalPayload(w io.Writer, models interface{}) error 230``` 231 232Visit [godoc](http://godoc.org/github.com/google/jsonapi#MarshalPayload) 233 234Writes a JSON API response, with related records sideloaded, into an 235`included` array. This method encodes a response for either a single record or 236many records. 237 238##### Handler Example Code 239 240```go 241func CreateBlog(w http.ResponseWriter, r *http.Request) { 242 blog := new(Blog) 243 244 if err := jsonapi.UnmarshalPayload(r.Body, blog); err != nil { 245 http.Error(w, err.Error(), http.StatusInternalServerError) 246 return 247 } 248 249 // ...save your blog... 250 251 w.Header().Set("Content-Type", jsonapi.MediaType) 252 w.WriteHeader(http.StatusCreated) 253 254 if err := jsonapi.MarshalPayload(w, blog); err != nil { 255 http.Error(w, err.Error(), http.StatusInternalServerError) 256 } 257} 258``` 259 260### Create Records Example 261 262#### `UnmarshalManyPayload` 263 264```go 265UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) 266``` 267 268Visit [godoc](http://godoc.org/github.com/google/jsonapi#UnmarshalManyPayload) 269 270Takes an `io.Reader` and a `reflect.Type` representing the uniform type 271contained within the `"data"` JSON API member. 272 273##### Handler Example Code 274 275```go 276func CreateBlogs(w http.ResponseWriter, r *http.Request) { 277 // ...create many blogs at once 278 279 blogs, err := UnmarshalManyPayload(r.Body, reflect.TypeOf(new(Blog))) 280 if err != nil { 281 t.Fatal(err) 282 } 283 284 for _, blog := range blogs { 285 b, ok := blog.(*Blog) 286 // ...save each of your blogs 287 } 288 289 w.Header().Set("Content-Type", jsonapi.MediaType) 290 w.WriteHeader(http.StatusCreated) 291 292 if err := jsonapi.MarshalPayload(w, blogs); err != nil { 293 http.Error(w, err.Error(), http.StatusInternalServerError) 294 } 295} 296``` 297 298 299### Links 300 301If 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: 302 303```go 304func (post Post) JSONAPILinks() *Links { 305 return &Links{ 306 "self": "href": fmt.Sprintf("https://example.com/posts/%d", post.ID), 307 "comments": Link{ 308 Href: fmt.Sprintf("https://example.com/api/blogs/%d/comments", post.ID), 309 Meta: map[string]interface{}{ 310 "counts": map[string]uint{ 311 "likes": 4, 312 }, 313 }, 314 }, 315 } 316} 317 318// Invoked for each relationship defined on the Post struct when marshaled 319func (post Post) JSONAPIRelationshipLinks(relation string) *Links { 320 if relation == "comments" { 321 return &Links{ 322 "related": fmt.Sprintf("https://example.com/posts/%d/comments", post.ID), 323 } 324 } 325 return nil 326} 327``` 328 329### Meta 330 331 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: 332 333 ```go 334func (post Post) JSONAPIMeta() *Meta { 335 return &Meta{ 336 "details": "sample details here", 337 } 338} 339 340// Invoked for each relationship defined on the Post struct when marshaled 341func (post Post) JSONAPIRelationshipMeta(relation string) *Meta { 342 if relation == "comments" { 343 return &Meta{ 344 "this": map[string]interface{}{ 345 "can": map[string]interface{}{ 346 "go": []interface{}{ 347 "as", 348 "deep", 349 map[string]interface{}{ 350 "as": "required", 351 }, 352 }, 353 }, 354 }, 355 } 356 } 357 return nil 358} 359``` 360 361### Custom types 362 363Custom types are supported for primitive types, only, as attributes. Examples, 364 365```go 366type CustomIntType int 367type CustomFloatType float64 368type CustomStringType string 369``` 370 371Types like following are not supported, but may be in the future: 372 373```go 374type CustomMapType map[string]interface{} 375type CustomSliceMapType []map[string]interface{} 376``` 377 378### Errors 379This package also implements support for JSON API compatible `errors` payloads using the following types. 380 381#### `MarshalErrors` 382```go 383MarshalErrors(w io.Writer, errs []*ErrorObject) error 384``` 385 386Writes a JSON API response using the given `[]error`. 387 388#### `ErrorsPayload` 389```go 390type ErrorsPayload struct { 391 Errors []*ErrorObject `json:"errors"` 392} 393``` 394 395ErrorsPayload is a serializer struct for representing a valid JSON API errors payload. 396 397#### `ErrorObject` 398```go 399type ErrorObject struct { ... } 400 401// Error implements the `Error` interface. 402func (e *ErrorObject) Error() string { 403 return fmt.Sprintf("Error: %s %s\n", e.Title, e.Detail) 404} 405``` 406 407ErrorObject is an `Error` implementation as well as an implementation of the JSON API error object. 408 409The 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. 410 411##### Errors Example Code 412```go 413// An error has come up in your code, so set an appropriate status, and serialize the error. 414if err := validate(&myStructToValidate); err != nil { 415 context.SetStatusCode(http.StatusBadRequest) // Or however you need to set a status. 416 jsonapi.MarshalErrors(w, []*ErrorObject{{ 417 Title: "Validation Error", 418 Detail: "Given request body was invalid.", 419 Status: "400", 420 Meta: map[string]interface{}{"field": "some_field", "error": "bad type", "expected": "string", "received": "float64"}, 421 }}) 422 return 423} 424``` 425 426## Testing 427 428### `MarshalOnePayloadEmbedded` 429 430```go 431MarshalOnePayloadEmbedded(w io.Writer, model interface{}) error 432``` 433 434Visit [godoc](http://godoc.org/github.com/google/jsonapi#MarshalOnePayloadEmbedded) 435 436This method is not strictly meant to for use in implementation code, 437although feel free. It was mainly created for use in tests; in most cases, 438your request payloads for create will be embedded rather than sideloaded 439for related records. This method will serialize a single struct pointer 440into an embedded json response. In other words, there will be no, 441`included`, array in the json; all relationships will be serialized 442inline with the data. 443 444However, in tests, you may want to construct payloads to post to create 445methods that are embedded to most closely model the payloads that will 446be produced by the client. This method aims to enable that. 447 448### Example 449 450```go 451out := bytes.NewBuffer(nil) 452 453// testModel returns a pointer to a Blog 454jsonapi.MarshalOnePayloadEmbedded(out, testModel()) 455 456h := new(BlogsHandler) 457 458w := httptest.NewRecorder() 459r, _ := http.NewRequest(http.MethodPost, "/blogs", out) 460 461h.CreateBlog(w, r) 462 463blog := new(Blog) 464jsonapi.UnmarshalPayload(w.Body, blog) 465 466// ... assert stuff about blog here ... 467``` 468 469## Alternative Installation 470I use git subtrees to manage dependencies rather than `go get` so that 471the src is committed to my repo. 472 473``` 474git subtree add --squash --prefix=src/github.com/google/jsonapi https://github.com/google/jsonapi.git master 475``` 476 477To update, 478 479``` 480git subtree pull --squash --prefix=src/github.com/google/jsonapi https://github.com/google/jsonapi.git master 481``` 482 483This assumes that I have my repo structured with a `src` dir containing 484a collection of packages and `GOPATH` is set to the root 485folder--containing `src`. 486 487## Contributing 488 489Fork, Change, Pull Request *with tests*. 490