• Home
  • History
  • Annotate
Name Date Size #Lines LOC

..03-May-2022-

examples/H18-Jun-2018-517393

.gitignoreH A D18-Jun-201819 21

.travis.ymlH A D18-Jun-201881 87

LICENSEH A D18-Jun-20181.1 KiB2217

README.mdH A D18-Jun-201814.1 KiB458351

constants.goH A D18-Jun-20181.9 KiB5622

doc.goH A D18-Jun-20182.8 KiB711

errors.goH A D18-Jun-20182.1 KiB5626

errors_test.goH A D18-Jun-20181.6 KiB5850

models_test.goH A D18-Jun-20184.9 KiB194170

node.goH A D18-Jun-20183.8 KiB12273

request.goH A D18-Jun-201817 KiB681498

request_test.goH A D18-Jun-201829.4 KiB1,2771,084

response.goH A D18-Jun-201813.7 KiB540378

response_test.goH A D18-Jun-201822.3 KiB971805

runtime.goH A D18-Jun-20182.2 KiB10477

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