jsonapi offers a set of tools to build JSON:API compliant services.
The official JSON:API specification can be found at jsonapi.org/format.
jsonapi offers the following features:
- Marshaling and unmarshaling of JSON:API URLs and documents
- Structs for handling URLs, documents, resources, collections...
- Schema management
- It can ensure relationships between types make sense.
- Very useful for validation when marshaling and unmarshaling.
- Utilities for pagination, sorting, and filtering
- jsonapi is opiniated when it comes to those features. If you prefer you own strategy fo pagination, sorting, and filtering, it will have to be done manually.
- In-memory data store (
SoftCollection
)- It can store resources (anything that implements
Resource
). - It can sort, filter, retrieve pages, etc.
- Enough to build a demo API or use in test suites.
- Not made for production use.
- It can store resources (anything that implements
- Other useful helpers
The library is in beta and its API is subject to change until v1 is released.
In terms of features, jsonapi is complete. The work left is polishing and testing the design of current API.
While anything can happen before a v1 release, the API is stable and no big changes are expected at this moment.
A few tasks are required before committing to the current API:
- Rethink how errors are handled
- Use the new tools introduced in Go 1.13.
- Simplify the API
- Remove anything that is redundant or not useful.
- Gather feedback from users
- The library should be used more on real projects to see of the API is convenient.
The supported versions of Go are the latest patch releases of every minor release starting with Go 1.13.
The best way to learn and appreciate this package is to look at the simple examples provided in the examples/
directory.
The simplest way to start using jsonapi is to use the MarshalDocument and UnmarshalDocument functions.
func MarshalDocument(doc *Document, url *URL) ([]byte, error)
func UnmarshalDocument(payload []byte, schema *Schema) (*Document, error)
A struct has to follow certain rules in order to be understood by the library, but interfaces are also provided which let the library avoid the reflect package and be more efficient.
See the following section for more information about how to define structs for this library.
Here are some of the main concepts covered by the library.
A Request
represents an HTTP request structured in a format easily readable from a JSON:API point of view.
If you are familiar with the specification, reading the Request
struct and its fields (URL
, Document
, etc) should be straightforward.
A Schema
contains all the schema information for an API, like types, fields, relationships between types, and so on. See schema.go
and type.go
for more details.
This is really useful for many uses cases:
- Making sure the schema is coherent
- Validating resources
- Parsing documents and URLs
- And probably many more...
For example, when a request comes in, a Document
and a URL
can be created by parsing the request. By providing a schema, the parsing can fail if it finds some errors like a type that does not exist, a field of the wrong kind, etc. After that step, valid data can be assumed.
A JSON:API type is generally defined with a struct.
There needs to be an ID field of type string. The api
tag represents the name of the type.
type User struct {
ID string `json:"id" api:"users"` // ID is mandatory and the api tag sets the type
// Attributes
Name string `json:"name" api:"attr"` // attr means it is an attribute
BornAt time.Time `json:"born-at" api:"attr"`
// Relationships
Articles []string `json:"articles" api:"rel,articles"`
}
Other fields with the api
tag (attr
or rel
) can be added as attributes or relationships.
Attributes can be of the following types:
string
int, int8, int16, int32, int64
uint, uint8, uint16, uint32, uint64
bool
time.Time
[]byte
*string
*int, *int8, *int16, *int32, *int64
*uint, *uint8, *uint16, *uint32, *uint64
*bool
*time.Time
*[]byte
Using a pointer allows the field to be nil.
Relationships can be a bit tricky. To-one relationships are defined with a string and to-many relationships are defined with a slice of strings. They contain the IDs of the related resources. The api tag has to take the form of "rel,xxx[,yyy]" where yyy is optional. xxx is the type of the relationship and yyy is the name of the inverse relationship when dealing with a two-way relationship. In the following example, our Article struct defines a relationship named author of type users:
Author string `json:"author" api:"rel,users,articles"`
A struct can be wrapped using the Wrap
function which returns a pointer to a Wrapper
. A Wrapper
implements the Resource
interface and can be used with this library. Modifying a Wrapper will modify the underlying struct. The resource's type is defined from reflecting on the struct.
user := User{}
wrap := Wrap(&user)
wrap.Set("name", "Mike")
fmt.Printf(wrap.Get("name")) // Output: Mike
fmt.Printf(user.Name) // Output: Mike
A SoftResource is a struct whose type (name, attributes, and relationships) can be modified indefinitely just like its values. When an attribute or a relationship is added, the new value is the zero value of the field type. For example, if you add an attribute named my-attribute
of type string, then softresource.Get("my-attribute")
will return an empty string.
sr := SoftResource{}
sr.AddAttr(Attr{
Name: "attr",
Type: AttrTypeInt,
Nullable: false,
})
fmt.Println(sr.Get("attr")) // Output: 0
Take a look at the SoftCollection
struct for a similar concept applied to an entire collection of resources.
From a raw string that represents a URL, it is possible that create a SimpleURL
which contains the information stored in the URL in a structure that is easier to handle.
It is also possible to build a URL
from a Schema
and a SimpleURL
which contains additional information taken from the schema. NewURL
returns an error if the URL does not respect the schema.
Check out the documentation.
The best way to learn how to use it is to look at documentation, the examples, and the code itself.