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

..03-May-2022-

examples/H18-May-2021-517393

.gitignoreH A D18-May-202119 21

.travis.ymlH A D18-May-2021144 1413

LICENSEH A D18-May-20211.1 KiB2217

README.mdH A D18-May-202114.9 KiB490372

constants.goH A D18-May-20212.1 KiB6225

doc.goH A D18-May-20212.8 KiB711

errors.goH A D18-May-20212.1 KiB5323

errors_test.goH A D18-May-20211.6 KiB5850

go.modH A D18-May-202136 21

models_test.goH A D18-May-20215.4 KiB210182

node.goH A D18-May-20213.8 KiB12273

request.goH A D18-May-202118.2 KiB722519

request_test.goH A D18-May-202136.5 KiB1,5531,342

response.goH A D18-May-202114 KiB541378

response_test.goH A D18-May-202124.3 KiB1,026870

runtime.goH A D18-May-20213.4 KiB13077

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
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