Lightweight JSON API implementation for Go.
go get -u "github.com/pieoneers/jsonapi-go"
Go to jsonapi-go package directory and run:
go test
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"
}
}
}
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"
}
}
}
}
}
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"
}
}]
}
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
}
}
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.
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,
},
}
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)
}
}