This Go module provides a useful API to create JSON:API HTTP servers. The primary usage of this library is to facilitate transformation from flattened Go structs into the standardized JSON:API resource object.
Additionally, there are optional methods that can be implemented with structs to add further standardized JSON:API structures such as links, relationships, included data, and metadata.
go get github.com/alehechka/go-jsonapi
Import as:
import "github.com/alehechka/go-jsonapi/jsonapi"
The primary resource object in JSON:API is of the following type:
{
"data": {
"id": "1234",
"type": "people",
"attributes": {
"firstName": "John",
"lastName": "Doe",
"age": 30
}
}
}
- The
attributes
object will be generated from the struct itself. - The
id
field will be populated by theID()
interface method. - The
type
field will be populated by theType()
interface method.
type Person struct {
// It is recommended to omit the primary ID from json marshalling, but not required
PersonID string `json:"-"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Age int `json:"age"`
}
func (person Person) ID() string {
return person.PersonID
}
func (person Person) Type() string {
return "people"
}
To prepare the struct for json marshalling it is required to use the provided TransformResponse
or TransformCollectionResponse
functions:
response := jsonapi.TransformResponse(jsonapi.Response{
Node: Person{},
"http://example.com",
})
response := jsonapi.TransformCollectionResponse(jsonapi.CollectionResponse{
Nodes: []Person{},
"http://example.com",
})
The second parameter to these functions is for baseURL
, this is used to dynamically populate relative URLs in links
objects. More on this here.
The above functions are effectively the top-level transformation tools, however, the dynamic link creation can be made easy by supplying an *http.Request
object to the following functions instead:
req := httptest.NewRequest("GET", "http://example.com/example", nil)
response := jsonapi.CreateResponse(req)(jsonapi.Response{
Node: Person{},
})
response := jsonapi.CreateCollectionResponse(req)(jsonapi.CollectionResponse{
Nodes: []Person{},
})
These versions will automatically extract the baseURL from the request and supply it to the respective Transform
functions outlined above. This allows all generated links to display the same scheme and hostname as the server domain that the request was originally made to.
Additionally, using the Create
functions will automatically generate a self
link at the top-level object for every response.
The JSON:API spec also allows for links
, errors
, and meta
objects at the top-level of the document. Both jsonapi.Response
and jsonapi.CollectionResponse
have values available for these.
A top-level links
object can be provided to both Response
and CollectionResponse
. See Link below for further details.
res := jsonapi.Response{
Links: jsonapi.Links{
jsonapi.NextKey: jsonapi.Link{
Href: "/path/to/next/resource",
},
},
}
When using either
CreateResponse
orCreateCollectionResponse
theself
link will be automatically generated and always override an existingself
link.
A top-level meta
object can be provided to both Response
and CollectionResponse
in the form of any interface or key-value map.
res := jsonapi.Response{
Meta: jsonapi.Meta{
"page": jsonapi.Meta{
"size": 10,
"number": 2,
},
},
}
The
Meta
struct is simply an alias formap[string]interface{}
A top-level errors
array can be provided to both Response
and CollectionResponse
in the form of an array of Error
objects. See Error below for further detail.
res := jsonapi.Response{
Errors: jsonapi.Errors{
{
Status: http.StatusBadRequest,
Title: "Error Occurred",
Detail: "Failed to retrieve resource",
},
},
}
It is important to note that if at least 1 error is present in this array than the top-level
data
object/array andincluded
array will not be available as per the JSON:API spec for Top Level.
By default, to be considered a JSON:API resource, a struct must include the ID()
and Type()
methods.
However, this functionality can be extended further with other methods as follows:
The Links()
method allows an individual resource to generate the links
object for itself using data from the object. See Link below for further details.
func (person Person) Links() jsonapi.Links {
return jsonapi.Links{
jsonapi.SelfKey: jsonapi.Link{
Href: "/people/:id",
Params: jsonapi.Params{
"id": person.ID(),
}
},
}
}
The above scenario makes use of the Params
field which will not be included in the resulting json, but will use the key-value pairs to substitute the values into the href
based on keys that it finds. (Ex. :id
in the href will be substituted with the value of person.ID()
)
Relationships are a key object within a resource to provide linkage and information about related resources. To facilitate the mapping, the Relationships()
method gives access to the parent struct and allows definition of the relationships
map as follows:
type Company struct {
CompanyID string `json:"-"`
Name string `json:"name"`
Address string `json:"address"`
Employees []Person `json:"-"` // recommended to omit children resources
Owner Person `json:"-"`
}
func (company Company) Relationships() map[string]interface{
return map[string]interface{}{
"employees": company.Employees,
"owner": company.Owner,
}
}
In the above example it is crucial that the children relationship objects adhere to the JSON:API methods, i.e. initialize their own
ID()
andType()
methods.
Typically in the relationships
object, there will be included links
object with links to the related resources. This can be facilitated by included the RelationshipLinks(parentID string
) on children structs. The parentID
parameter will automatically be supplied when generated as part of a relationship by the parent struct, it is recommended to use this in generating path params for the href variable.
func (person Person) RelationshipLinks(companyID string) jsonapi.Links {
return jsonapi.Links{
jsonapi.SelfKey: jsonapi.Link{
Href: "/companies/:companyID/relationships/employees",
Params: jsonapi.Params{
"companyID": companyID,
},
},
jsonapi.RelatedKey: jsonapi.Link{
Href: "/companies/:companyID/employees",
Params: jsonapi.Params{
"companyID": companyID,
},
},
}
}
If the relationship will point to an array of resources, it is recommended to instead create a unique type for that array of structs as follows:
type People []Person
func (people People) RelationshipLinks(companyID string) jsonapi.Links {
return jsonapi.Links{
jsonapi.SelfKey: jsonapi.Link{
Href: "/companies/:companyID/relationships/employees",
Params: jsonapi.Params{
"companyID": companyID,
},
},
}
}
The Meta()
method is simply a means to generate a meta
object for an individual resource by using the object as an input.
func (person Person) Meta() interface{} {
return jsonapi.Meta{
"fullName": fmt.Sprintf("%s %s", person.FirstName, person.LastName),
}
}
The JSON:API Links states that each value of the links
map must either be a string containing the link's URL or an object with an href
and meta
object. By, default, a Link
object will be transformed into the string format in all cases expect when a non-nil, non-empty Meta
object is provided.
links := jsonapi.Links{
"self": jsonapi.Link{
Href: "/path/to/resource",
},
"next": jsonapi.Link{
Href: "/path/to/next/resource",
Meta: jsonapi.Meta{
"page": 3,
},
},
}
After transformation and JSON marshalling assuming the provided baseURL
is http://example.com
, the result will be as follows:
{
"links": {
"self": "http://example.com/path/to/resource",
"next": {
"href": "http://example.com/path/to/next/resource",
"meta": {
"page": 3
}
}
}
}
Additionally, the Link
object provides options for Params
and Queries
. These will always be ignored in the JSON marshalling and are used to help generate the href
URL.
Params
is a map of key-value pairs that represent path parameters. During transformation, href path sections that are prefixed with a colon (:
), will be substituted with the value of a matching key in theParams
map.Queries
is a map of key-value pairs that represent query parameters. During transformation, all key-value pairs will be generated and appended to the href as query parameters. Pre-existing query parameters in the supplied href will not be removed, but will be replaced if they have the same key.
links := jsonapi.Links{
"self": jsonapi.Link{
Href: "/path/to/resource/:id?page[size]=20"
Params: jsonapi.Params{
"id": 1234,
},
Queries: jsonapi.Queries{
"page[number]": 4,
},
},
}
After transformation and JSON marshalling assuming the provided baseURL
is http://example.com
, the result will be as follows:
{
"links": {
"self": "http://example.com/path/to/resource/1234?page[size]=20&page[limit]=4"
}
}
For further details, view the implementation here: /jsonapi/links.go
The JSON:API Errors
specification includes a large number of fields, all of which can be supplied to the provided Error
object. The internal links
object of Error
will also be supplied the baseURL
and follow the same transformation rules outlined above.
errs := jsonapi.Errors{
{
Status: http.StatusBadRequest,
Title: "Standard Error Occurred",
Detail: "Further Detail is supplied here",
},
}
After transformation and JSON marshalling, the result will be as follows:
{
"errors": [
{
"status": 400,
"title": "Standard Error Occurred",
"detail": "Further Detail is supplied here"
}
]
}
For further details, view the implementation here: /jsonapi/errors.go