Skip to content
This repository was archived by the owner on Sep 4, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 29 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![Build Status](https://travis-ci.org/google/jsonapi.svg?branch=master)](https://travis-ci.org/google/jsonapi) [![GoDoc](https://godoc.org/github.com/google/jsonapi?status.svg)](http://godoc.org/github.com/google/jsonapi)

A serializer/deserializer for json payloads that comply to the
A serializer/deserializer for JSON payloads that comply to the
[JSON API - jsonapi.org](http://jsonapi.org) spec in go.

## Installation
Expand Down Expand Up @@ -365,26 +365,36 @@ func (post Post) JSONAPIRelationshipLinks(relation string) *Links {
```

### Meta

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:

```go
func (post Post) JSONAPIMeta() *Meta {
return &Meta{
"details": "sample details here",
}
}

// Invoked for each relationship defined on the Post struct when marshaled
func (post Post) JSONAPIRelationshipMeta(relation string) *Meta {
if relation == "comments" {
return &Meta{
"details": "comment meta details here",
}
}
return nil
}
```
func (post Post) JSONAPIMeta() *Meta {
return &Meta{
"details": "sample details here",
}
}

// Invoked for each relationship defined on the Post struct when marshaled
func (post Post) JSONAPIRelationshipMeta(relation string) *Meta {
if relation == "comments" {
return &Meta{
"this": map[string]interface{}{
"can": map[string]interface{}{
"go": []interface{}{
"as",
"deep",
map[string]interface{}{
"as": "required",
},
},
},
},
}
}
return nil
}
```

### Errors
This package also implements support for JSON API compatible `errors` payloads using the following types.
Expand Down
8 changes: 4 additions & 4 deletions examples/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func exerciseHandler() {
// list
req, _ := http.NewRequest(http.MethodGet, "/blogs", nil)

req.Header.Set("Accept", jsonapi.MediaType)
req.Header.Set(headerAccept, jsonapi.MediaType)

w := httptest.NewRecorder()

Expand All @@ -60,7 +60,7 @@ func exerciseHandler() {
// show
req, _ = http.NewRequest(http.MethodGet, "/blogs?id=1", nil)

req.Header.Set("Accept", jsonapi.MediaType)
req.Header.Set(headerAccept, jsonapi.MediaType)

w = httptest.NewRecorder()

Expand All @@ -81,7 +81,7 @@ func exerciseHandler() {

req, _ = http.NewRequest(http.MethodPost, "/blogs", in)

req.Header.Set("Accept", jsonapi.MediaType)
req.Header.Set(headerAccept, jsonapi.MediaType)

w = httptest.NewRecorder()

Expand All @@ -107,7 +107,7 @@ func exerciseHandler() {

req, _ = http.NewRequest(http.MethodPut, "/blogs", in)

req.Header.Set("Accept", jsonapi.MediaType)
req.Header.Set(headerAccept, jsonapi.MediaType)

w = httptest.NewRecorder()

Expand Down
33 changes: 28 additions & 5 deletions examples/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package main
import (
"fmt"
"time"

"github.com/google/jsonapi"
)

type Blog struct {
Expand Down Expand Up @@ -30,22 +32,43 @@ type Comment struct {
}

// Blog Links
func (blog Blog) JSONAPILinks() *map[string]interface{} {
return &map[string]interface{}{
func (blog Blog) JSONAPILinks() *jsonapi.Links {
return &jsonapi.Links{
"self": fmt.Sprintf("https://example.com/blogs/%d", blog.ID),
}
}

func (blog Blog) JSONAPIRelationshipLinks(relation string) *map[string]interface{} {
func (blog Blog) JSONAPIRelationshipLinks(relation string) *jsonapi.Links {
if relation == "posts" {
return &map[string]interface{}{
return &jsonapi.Links{
"related": fmt.Sprintf("https://example.com/blogs/%d/posts", blog.ID),
}
}
if relation == "current_post" {
return &map[string]interface{}{
return &jsonapi.Links{
"related": fmt.Sprintf("https://example.com/blogs/%d/current_post", blog.ID),
}
}
return nil
}

// Blog Meta
func (blog Blog) JSONAPIMeta() *jsonapi.Meta {
return &jsonapi.Meta{
"detail": "extra details regarding the blog",
}
}

func (blog Blog) JSONAPIRelationshipMeta(relation string) *jsonapi.Meta {
if relation == "posts" {
return &jsonapi.Meta{
"detail": "posts meta information",
}
}
if relation == "current_post" {
return &jsonapi.Meta{
"detail": "current post meta information",
}
}
return nil
}
157 changes: 157 additions & 0 deletions models_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package jsonapi

import (
"fmt"
"time"
)

type BadModel struct {
ID int `jsonapi:"primary"`
}

type ModelBadTypes struct {
ID string `jsonapi:"primary,badtypes"`
StringField string `jsonapi:"attr,string_field"`
FloatField float64 `jsonapi:"attr,float_field"`
TimeField time.Time `jsonapi:"attr,time_field"`
TimePtrField *time.Time `jsonapi:"attr,time_ptr_field"`
}

type WithPointer struct {
ID *uint64 `jsonapi:"primary,with-pointers"`
Name *string `jsonapi:"attr,name"`
IsActive *bool `jsonapi:"attr,is-active"`
IntVal *int `jsonapi:"attr,int-val"`
FloatVal *float32 `jsonapi:"attr,float-val"`
}

type Timestamp struct {
ID int `jsonapi:"primary,timestamps"`
Time time.Time `jsonapi:"attr,timestamp,iso8601"`
Next *time.Time `jsonapi:"attr,next,iso8601"`
}

type Car struct {
ID *string `jsonapi:"primary,cars"`
Make *string `jsonapi:"attr,make,omitempty"`
Model *string `jsonapi:"attr,model,omitempty"`
Year *uint `jsonapi:"attr,year,omitempty"`
}

type Post struct {
Blog
ID uint64 `jsonapi:"primary,posts"`
BlogID int `jsonapi:"attr,blog_id"`
ClientID string `jsonapi:"client-id"`
Title string `jsonapi:"attr,title"`
Body string `jsonapi:"attr,body"`
Comments []*Comment `jsonapi:"relation,comments"`
LatestComment *Comment `jsonapi:"relation,latest_comment"`
}

type Comment struct {
ID int `jsonapi:"primary,comments"`
ClientID string `jsonapi:"client-id"`
PostID int `jsonapi:"attr,post_id"`
Body string `jsonapi:"attr,body"`
}

type Book struct {
ID uint64 `jsonapi:"primary,books"`
Author string `jsonapi:"attr,author"`
ISBN string `jsonapi:"attr,isbn"`
Title string `jsonapi:"attr,title,omitempty"`
Description *string `jsonapi:"attr,description"`
Pages *uint `jsonapi:"attr,pages,omitempty"`
PublishedAt time.Time
Tags []string `jsonapi:"attr,tags"`
}

type Blog struct {
ID int `jsonapi:"primary,blogs"`
ClientID string `jsonapi:"client-id"`
Title string `jsonapi:"attr,title"`
Posts []*Post `jsonapi:"relation,posts"`
CurrentPost *Post `jsonapi:"relation,current_post"`
CurrentPostID int `jsonapi:"attr,current_post_id"`
CreatedAt time.Time `jsonapi:"attr,created_at"`
ViewCount int `jsonapi:"attr,view_count"`
}

func (b *Blog) JSONAPILinks() *Links {
return &Links{
"self": fmt.Sprintf("https://example.com/api/blogs/%d", b.ID),
"comments": Link{
Href: fmt.Sprintf("https://example.com/api/blogs/%d/comments", b.ID),
Meta: Meta{
"counts": map[string]uint{
"likes": 4,
"comments": 20,
},
},
},
}
}

func (b *Blog) JSONAPIRelationshipLinks(relation string) *Links {
if relation == "posts" {
return &Links{
"related": Link{
Href: fmt.Sprintf("https://example.com/api/blogs/%d/posts", b.ID),
Meta: Meta{
"count": len(b.Posts),
},
},
}
}
if relation == "current_post" {
return &Links{
"self": fmt.Sprintf("https://example.com/api/posts/%s", "3"),
"related": Link{
Href: fmt.Sprintf("https://example.com/api/blogs/%d/current_post", b.ID),
},
}
}
return nil
}

func (b *Blog) JSONAPIMeta() *Meta {
return &Meta{
"detail": "extra details regarding the blog",
}
}

func (b *Blog) JSONAPIRelationshipMeta(relation string) *Meta {
if relation == "posts" {
return &Meta{
"this": map[string]interface{}{
"can": map[string]interface{}{
"go": []interface{}{
"as",
"deep",
map[string]interface{}{
"as": "required",
},
},
},
},
}
}
if relation == "current_post" {
return &Meta{
"detail": "extra current_post detail",
}
}
return nil
}

type BadComment struct {
ID uint64 `jsonapi:"primary,bad-comment"`
Body string `jsonapi:"attr,body"`
}

func (bc *BadComment) JSONAPILinks() *Links {
return &Links{
"self": []string{"invalid", "should error"},
}
}
14 changes: 8 additions & 6 deletions node.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,6 @@ type RelationshipManyNode struct {
// http://jsonapi.org/format/#document-links
type Links map[string]interface{}

// Meta is used to represent a `meta` object.
// http://jsonapi.org/format/#document-meta
type Meta map[string]interface{}

func (l *Links) validate() (err error) {
// Each member of a links object is a “link”. A link MUST be represented as
// either:
Expand All @@ -78,8 +74,8 @@ func (l *Links) validate() (err error) {

// Link is used to represent a member of the `links` object.
type Link struct {
Href string `json:"href"`
Meta map[string]interface{} `json:"meta,omitempty"`
Href string `json:"href"`
Meta Meta `json:"meta,omitempty"`
}

// Linkable is used to include document links in response data
Expand All @@ -95,6 +91,12 @@ type RelationshipLinkable interface {
JSONAPIRelationshipLinks(relation string) *Links
}

// Meta is used to represent a `meta` object.
// http://jsonapi.org/format/#document-meta
type Meta map[string]interface{}

// Metable is used to include document meta in response data
// e.g. {"foo": "bar"}
type Metable interface {
JSONAPIMeta() *Meta
}
Expand Down
20 changes: 0 additions & 20 deletions request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,6 @@ import (
"time"
)

type BadModel struct {
ID int `jsonapi:"primary"`
}

type WithPointer struct {
ID *uint64 `jsonapi:"primary,with-pointers"`
Name *string `jsonapi:"attr,name"`
IsActive *bool `jsonapi:"attr,is-active"`
IntVal *int `jsonapi:"attr,int-val"`
FloatVal *float32 `jsonapi:"attr,float-val"`
}

type ModelBadTypes struct {
ID string `jsonapi:"primary,badtypes"`
StringField string `jsonapi:"attr,string_field"`
FloatField float64 `jsonapi:"attr,float_field"`
TimeField time.Time `jsonapi:"attr,time_field"`
TimePtrField *time.Time `jsonapi:"attr,time_ptr_field"`
}

func TestUnmarshall_attrStringSlice(t *testing.T) {
out := &Book{}
tags := []string{"fiction", "sale"}
Expand Down
Loading