Go jsonapi client
Lightweight JSON API implementation for Go.
Installing
go get -u "github.com/pieoneers/jsonapi-go"
Running the tests
Go to jsonapi-go package directory and run:
go test
Usage
Marshal
For instance we have Go program what implements a simple library, we have Book and Author structs:
type Book struct { ID string AuthorID string Title string PublicationDate time.Time } type Author struct { ID string FirstName string LastName string }
We want to produce the JSON representation of the data. Let's modify the struct by adding json tags to it and implement functions GetID and GetType as required for MarshalResourceIdentifier interface, one more function GetData required for MarshalData interface.
type Book struct { ID string `json:"-"` AuthorID string `json:"-"` Title string `json:"title"` PublicationDate time.Time `json:"publication_date"` } func (b Book) GetID() string { return b.ID } func (b Book) GetType() string { return "books" } func (b Book) GetData() interface{} { return b } type Author struct { ID string `json:"-"` FirstName string `json:"first_name"` LastName string `json:"last_name"` } func (a Author) GetID() string { return a.ID } func (a Author) GetType() string { return "authors" } func (a Author) GetData() interface{} { return a }
By running Marshal function for Book and Author the output will be a []byte of json data.
Initial data:
alan := Author{ ID: "1", FirstName: "Alan A. A.", LastName: "Donovan", } publicationDate, _ := time.Parse(time.RFC3339, "2015-01-01T00:00:00Z") book := Book{ ID: "1", Title: "Go Programming Language", AuthorID: alan.ID, PublicationDate: publicationDate, }
Running Marshal:
bookJSON, _ := jsonapi.Marshal(book1) authorJSON, _ := jsonapi.Marshal(alan)
Output:
{
"data": {
"type": "books",
"id": "1",
"attributes": {
"title": "Go Programming Language",
"publication_date": "2015-01-01T00:00:00Z"
}
}
}and
{
"data": {
"type": "authors",
"id": "1",
"attributes": {
"first_name": "Alan A. A.",
"last_name": "Donovan"
}
}
}Relationships
Add relationships to the resource is easy to do by implementing MarshalRelationships interface
e.g. for Book we will add GetRelationships function.
func (b Book) GetRelationships() map[string]interface{} { relationships := make(map[string]interface{}) relationships["author"] = jsonapi.ResourceObjectIdentifier{ ID: b.AuthorID, Type: "authors", } return relationships }
The Marshal output will be:
{
"data": {
"type": "books",
"id": "1",
"attributes": {
"title": "Go Programming Language",
"publication_date": "2015-01-01T00:00:00Z"
},
"relationships": {
"author": {
"data": {
"type": "authors",
"id": "1"
}
}
}
}
}Included
Adding to JSON document included is easy by adding function GetIncluded it will implement MarshalIncluded interface.
func (b Book) GetIncluded() []interface{} { var included []interface{} //`authors` a global array with all authors, but it can be a DB or something else for _, author := range authors { if author.ID == b.AuthorID { included = append(included, author) } } return included }
When JSON document is a collection of resources better way to implement additional data type and Books method GetIncluded, otherwise all same included resources may be included several times.
type Books []Book func (b Books) GetIncluded() []interface{} { //do something }
Book with included will be looks like:
{
"data": {
"type": "books",
"id": "1",
"attributes": {
"title": "Go Programming Language",
"publication_date": "2015-01-01T00:00:00Z"
},
"relationships": {
"author": {
"data": {
"type": "authors",
"id": "1"
}
}
}
},
"included": [{
"type": "authors",
"id": "1",
"attributes": {
"first_name": "Alan A. A.",
"last_name": "Donovan"
}
}]
}Meta
Book's GetMeta will implement MarshalMeta interface.
meta section will be added to json document.
func (b Book) GetMeta() interface{} { return Meta{ ReadCount: 42 } }
the output:
{
"data": {
"type": "books",
"id": "1",
"attributes": {
"title": "Go Programming Language",
"publication_date": "2015-01-01T00:00:00Z"
},
"meta": {
"read_count": 42
},
"relationships": {
"author": {
"data": {
"type": "authors",
"id": "1"
}
}
}
},
"included": [{
"type": "authors",
"id": "1",
"attributes": {
"first_name": "Alan A. A.",
"last_name": "Donovan"
}
}],
"meta": {
"read_count": 42
}
}Here we can find that meta includes in resource object and into JSON document.
It happened, because resource and document may have their own meta, to avoid such behavior we should implement Book document data type and implement GetMeta for the Book's document.
type BookDocumentMeta struct { TotalCount int `json:"total_count"` } type BookDocument struct { Data Book Meta BookDocumentMeta } func (b BookDocument) GetData() interface{} { return b.Data } func (b BookDocument) GetMeta() interface{} { return b.Meta } ... bookJSON, _ := jsonapi.Marshal(BookDocument{ Data: book, //book from previous examples Meta: BookDocumentMeta{ 17 }, //let's imagine that we have 17 books in our library })
Output:
{
"data": {
"type": "books",
"id": "1",
"attributes": {
"title": "Go Programming Language",
"publication_date": "2015-01-01T00:00:00Z"
},
"meta": {
"read_count": 42
},
"relationships": {
"author": {
"data": {
"type": "authors",
"id": "1"
}
}
}
},
"meta": {
"total_count": 17
}
}Unmarshal
Now we will implement the examples, how to put values from JSON document into Go struct.
We will use the same data types Book and Author
But the set of functions will be different.
Book:
type Book struct { ID string `json:"-"` Type string `json:"-"` AuthorID string `json:"-"` Title string `json:"title"` PublicationDate time.Time `json:"publication_date"` } func (b *Book) SetID(id string) error { b.ID = id return nil } func (b *Book) SetType(t string) error { b.Type = t return nil } func (b *Book) SetData(to func(target interface{}) error) error { return to(b) }
Here is JSON data:
{
"data": {
"type": "books",
"id": "1",
"attributes": {
"title": "Go Programming Language",
"publication_date": "2015-01-01T00:00:00Z"
},
"relationships": {
"author": {
"data": {
"type": "authors",
"id": "1"
}
}
}
}
}Let's call Unmarshal function.
book := Book{} jsonapi.Unmarshal(bookJSON, &book) //bookJSON is []byte of JSON data.
At the end we will have Go struct:
_ = Book{ ID: "1", Type: "books", AuthorID: "", Title: "Go Programming Language" PublicationDate: 2015-01-01 00:00:00 +0000 UTC, }
But the AuthorID is empty, to set this relationship we should implement UnmarshalRelationships interface, by creating function SetRelationships:
func (b *Book) SetRelationships(relationships map[string]interface{}) error { if relationship, ok := relationships["author"]; ok { b.AuthorID = relationship.(*jsonapi.ResourceObjectIdentifier).ID } return nil }
call Unmarshal again and look at the result.
Collection unmarshal
When you need to unmarshal collections, you should implement SetData function for collection datatype.
type Books []Book func (b *Books) SetData(to func(target interface{}) error) error { return to(b) }
If resource Book data type have SetRelationships function, in the collection all relationships will be filled.
Example:
var booksJSON = []byte(` { "data": [ { "type": "books", "id": "1", "attributes": { "title": "Go Programming Language", "publication_date": "2015-01-01T00:00:00Z" }, "relationships": { "author": { "data": { "type": "authors", "id": "1" } } } }, { "type": "books", "id": "2", "attributes": { "title": "Learning Functional Programming in Go", "publication_date": "2017-11-01T00:00:00Z" }, "relationships": { "author": { "data": { "type": "authors", "id": "2" } } } }, { "type": "books", "id": "3", "attributes": { "title": "Go in Action", "publication_date": "2015-11-01T00:00:00Z" }, "relationships": { "author": { "data": { "type": "authors", "id": "3" } } } } ] } `) books := Books{} jsonapi.Unmarshal(booksJSON, &books)
The output:
_ = Books{ { ID: "1", Type: "books", AuthorID: "1", Title: "Go Programming Language", PublicationDate: 2015-01-01 00:00:00 +0000 UTC, }, { ID: "2", Type: "books", AuthorID: "2", Title: "Learning Functional Programming in Go", PublicationDate: 2017-11-01 00:00:00 +0000 UTC, }, { ID: "3", Type: "books", AuthorID: "3", Title: "Go in Action", PublicationDate: 2015-11-01 00:00:00 +0000 UTC, }, }
Example Library client and server applications.
server.go
package main import ( "github.com/pieoneers/jsonapi-go" "log" "net/http" "time" ) type Meta struct { Count int `json:"count"` } type Book struct { ID string `json:"-"` AuthorID string `json:"-"` Title string `json:"title"` PublicationDate time.Time `json:"publication_date"` } func (b Book) GetID() string { return b.ID } func (b Book) GetType() string { return "books" } func (b Book) GetData() interface{} { return b } func (b Book) GetRelationships() map[string]interface{} { relationships := make(map[string]interface{}) relationships["author"] = jsonapi.ResourceObjectIdentifier{ ID: b.AuthorID, Type: "authors", } return relationships } func (b Book) GetIncluded() []interface{} { var included []interface{} for _, author := range authors { if author.ID == b.AuthorID { included = append(included, author) } } return included } type Books []Book func (b Books) GetData() interface{} { return b } func (b Books) GetMeta() interface{} { return Meta{Count: len(books)} } func (b Books) GetIncluded() []interface{} { var included []interface{} authorsMap := make(map[string]Author) for _, book := range b { for _, author := range authors { if book.AuthorID == author.ID { authorsMap[author.ID] = author } } } for _, author := range authorsMap { included = append(included, author) } return included } type Author struct { ID string `json:"-"` FirstName string `json:"first_name"` LastName string `json:"last_name"` } func (a Author) GetID() string { return a.ID } func (a Author) GetType() string { return "authors" } func (a Author) GetData() interface{} { return a } func (a Author) GetIncluded() []interface{} { var included []interface{} for _, book := range books { if book.AuthorID == a.ID { included = append(included, book) } } return included } type Authors []Author func (a Authors) GetMeta() interface{} { return Meta{Count: len(authors)} } func (a Authors) GetData() interface{} { return a } func (a Authors) GetIncluded() []interface{} { var included []interface{} booksMap := make(map[string]Book) for _, author := range a { for _, book := range books { if book.AuthorID == author.ID { booksMap[book.ID] = book } } } for _, book := range booksMap { included = append(included, book) } return included } var ( authors Authors books Books ) func bookHandler(w http.ResponseWriter, req *http.Request) { id := req.URL.Path[len("/books/"):] for _, book := range books { if book.ID == id { bookData, err := jsonapi.Marshal(book) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/vnd.api+json") w.Write(bookData) w.WriteHeader(http.StatusOK) return } } w.WriteHeader(http.StatusNotFound) } func booksHandler(w http.ResponseWriter, req *http.Request) { booksData, err := jsonapi.Marshal(books) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/vnd.api+json") w.Write(booksData) w.WriteHeader(http.StatusOK) } func authorHandler(w http.ResponseWriter, req *http.Request) { id := req.URL.Path[len("/authors/"):] for _, author := range authors { if author.ID == id { authorData, err := jsonapi.Marshal(author) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/vnd.api+json") w.Write(authorData) w.WriteHeader(http.StatusOK) return } } w.WriteHeader(http.StatusNotFound) } func authorsHandler(w http.ResponseWriter, req *http.Request) { authorsData, err := jsonapi.Marshal(authors) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/vnd.api+json") w.Write(authorsData) w.WriteHeader(http.StatusOK) } func main() { var publicationDate time.Time alan := Author{ID: "1", FirstName: "Alan A. A.", LastName: "Donovan"} authors = append(authors, alan) lex := Author{ID: "2", FirstName: "Lex", LastName: "Sheehan"} authors = append(authors, lex) william := Author{ID: "3", FirstName: "William", LastName: "Kennedy"} authors = append(authors, william) publicationDate, _ = time.Parse(time.RFC3339, "2015-01-01T00:00:00Z") book1 := Book{ ID: "1", Title: "Go Programming Language", AuthorID: alan.ID, PublicationDate: publicationDate, } books = append(books, book1) publicationDate, _ = time.Parse(time.RFC3339, "2017-11-01T00:00:00Z") book2 := Book{ ID: "2", Title: "Learning Functional Programming in Go", AuthorID: lex.ID, PublicationDate: publicationDate, } books = append(books, book2) publicationDate, _ = time.Parse(time.RFC3339, "2015-11-01T00:00:00Z") book3 := Book{ ID: "3", Title: "Go in Action", AuthorID: william.ID, PublicationDate: publicationDate, } books = append(books, book3) http.HandleFunc("/books/", bookHandler) http.HandleFunc("/books", booksHandler) http.HandleFunc("/authors/", authorHandler) http.HandleFunc("/authors", authorsHandler) log.Fatal(http.ListenAndServe(":8080", nil)) }
client.go
package main import ( "fmt" "io/ioutil" "log" "net/http" "time" "github.com/pieoneers/jsonapi-go" ) type Book struct { ID string `json:"-"` Type string `json:"-"` AuthorID string `json:"-"` Title string `json:"title"` PublicationDate time.Time `json:"publication_date"` } func(b *Book) SetID(id string) error { b.ID = id return nil } func(b *Book) SetType(t string) error { b.Type = t return nil } func(b *Book) SetData(to func(target interface{}) error) error { return to(b) } func(b *Book) SetRelationships(relationships map[string]interface{}) error { if relationship, ok := relationships["author"]; ok { b.AuthorID = relationship.(*jsonapi.ResourceObjectIdentifier).ID } return nil } type Books []Book func(b *Books) SetData(to func(target interface{}) error) error { return to(b) } type Author struct { ID string `json:"-"` Type string `json:"-"` FirstName string `json:"first_name"` LastName string `json:"last_name"` } func(a *Author) SetID(id string) error { a.ID = id return nil } func(a *Author) SetType(t string) error { a.Type = t return nil } func(a *Author) SetData(to func(target interface{}) error) error { return to(a) } type Authors []Author func(a *Authors) SetData(to func(target interface{}) error) error { return to(a) } func printBook(b Book) { fmt.Printf("ID:\t%v,\nType:\t%v\nAuthorID:\t%v\nTitle:\t%v\nPublicationDate:\t%v\n", b.ID, b.Type, b.AuthorID, b.Title, b.PublicationDate) } func printAuthor(a Author) { fmt.Printf("ID:\t%v,\nType:\t%v\nFirstName:\t%v\nLastName:\t%v\n", a.ID, a.Type, a.FirstName, a.LastName) } func GetBooks() (books Books){ res, err := http.Get("http://localhost:8080/books") if err != nil { log.Fatal(err) } if res.StatusCode != http.StatusOK { return } booksJSON, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { log.Fatal(err) } _, jsonapiErr := jsonapi.Unmarshal(booksJSON, &books) if jsonapiErr != nil { log.Fatal(jsonapiErr) } return books } func GetAuthors() (authors Authors){ res, err := http.Get("http://localhost:8080/authors") if err != nil { log.Fatal(err) } if res.StatusCode != http.StatusOK { return } authorsJSON, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { log.Fatal(err) } _, jsonapiErr := jsonapi.Unmarshal(authorsJSON, &authors) if jsonapiErr != nil { log.Fatal(jsonapiErr) } return authors } func main() { books := GetBooks() for _, book := range books { printBook(book) } authors := GetAuthors() for _, author := range authors { printAuthor(author) } }