/go-jsonapi

Go Module for creating JSON:API http servers

Primary LanguageGo

go-jsonapi

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.

Installation

go get github.com/alehechka/go-jsonapi

Import as:

import "github.com/alehechka/go-jsonapi/jsonapi"

Usage

Defining a JSON:API struct

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 the ID() interface method.
  • The type field will be populated by the Type() 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"
}

Prepare for JSON marshalling

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.

Recommended Usage

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.

Extending the top-level resource

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.

Links

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 or CreateCollectionResponse the self link will be automatically generated and always override an existing self link.

Meta

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 for map[string]interface{}

Errors

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 and included array will not be available as per the JSON:API spec for Top Level.

Extending Node interface

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:

Links()

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

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() and Type() methods.

RelationshipLinks(parentID string)

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

Meta()

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),
    }
}

Structs Explained

Link

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

Error

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