A comprehensive Go library for marshaling Go structs into JSON:API compliant resources and unmarshaling JSON:API documents back into Go structs. This library supports both automatic struct tag-based marshaling/unmarshaling and custom marshaling/unmarshaling through interfaces.
- âś… Full JSON:API specification compliance (marshaling & unmarshaling)
- âś… Struct tag-based automatic marshaling/unmarshaling
- âś… Custom marshaling/unmarshaling interfaces
- âś… Relationship handling with included resources
- âś… Embedded struct support
- âś… Context support for all operations
- âś… Thread-safe operations
- âś… Comprehensive error handling
- âś… Zero-value and omitempty support
- âś… Strict mode validation for unmarshaling
- âś… Type conversion during unmarshaling
- âś… HTTP server utilities for JSON:API endpoints
- âś… Request parameter parsing (sparse fieldsets, includes, sorting, pagination, filtering)
- âś… Content negotiation and proper header handling
- âś… Resource and relationship handlers for HTTP servers
- âś… Default HTTP routing for JSON:API endpoints
- âś… Iterator support for resource collections
- âś… 100% test coverage with comprehensive edge case validation
go get github.com/nisimpson/jsonapi
- Go 1.24.4 or higher
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/nisimpson/jsonapi"
)
// Define a struct with jsonapi tags
type User struct {
ID string `jsonapi:"primary,users"`
Name string `jsonapi:"attr,name"`
Email string `jsonapi:"attr,email,omitempty"`
}
func main() {
user := User{
ID: "123",
Name: "John Doe",
Email: "john@example.com",
}
// Marshal to JSON:API
data, err := jsonapi.Marshal(user)
if err != nil {
panic(err)
}
fmt.Println(string(data))
// Output:
// {"data":{"id":"123","type":"users","attributes":{"email":"john@example.com","name":"John Doe"}}}
}
package main
import (
"context"
"fmt"
"github.com/nisimpson/jsonapi"
)
// Define a struct with jsonapi tags
type User struct {
ID string `jsonapi:"primary,users"`
Name string `jsonapi:"attr,name"`
Email string `jsonapi:"attr,email,omitempty"`
}
func main() {
jsonData := []byte(`{
"data": {
"id": "123",
"type": "users",
"attributes": {
"name": "John Doe",
"email": "john@example.com"
}
}
}`)
var user User
err := jsonapi.Unmarshal(jsonData, &user)
if err != nil {
panic(err)
}
fmt.Printf("User: %s (%s)\n", user.Name, user.Email)
// Output:
// User: John Doe (john@example.com)
}
The library uses struct tags to determine how to marshal and unmarshal JSON:API resources:
type User struct {
ID string `jsonapi:"primary,users"` // Primary resource ID and type
Name string `jsonapi:"attr,name"` // Attribute
Email string `jsonapi:"attr,email,omitempty"` // Optional attribute
CreatedAt string `jsonapi:"attr,created_at,readonly"` // Read-only attribute
Posts []Post `jsonapi:"relation,posts"` // To-many relationship
Profile Profile `jsonapi:"relation,profile"` // To-one relationship
Author User `jsonapi:"relation,author,readonly"` // Read-only relationship
Metadata string `jsonapi:"-"` // Ignored field
}
primary,type
: Marks a field as the primary ID field and specifies the resource typeattr,name[,omitempty][,readonly]
: Marks a field as an attribute with optional flagsrelation,name[,omitempty][,readonly]
: Marks a field as a relationship with optional flags-
: Ignores the field during marshaling/unmarshaling
omitempty
: Omits the field during marshaling if it has a zero valuereadonly
: Tags the field as read-only (see below for details)
Fields marked with the readonly
tag option are marshaled normally but can fail unmarshaling if desired (see below). This is useful for server-generated fields like timestamps, computed values, or fields that should not be modified by clients:
type Article struct {
ID string `jsonapi:"primary,articles"`
Title string `jsonapi:"attr,title"`
Content string `jsonapi:"attr,content"`
CreatedAt time.Time `jsonapi:"attr,created_at,readonly"` // Server-generated timestamp
UpdatedAt time.Time `jsonapi:"attr,updated_at,readonly"` // Server-generated timestamp
Author User `jsonapi:"relation,author,readonly"` // Cannot be changed after creation
}
To prevent unmarshaling read-only fields (for example, when processing update requests), use the PermitReadOnly()
option:
var article Article
err := jsonapi.Unmarshal(data, &article, jsonapi.PermitReadOnly(false))
fmt.Println(errors.Is(err, jsonapi.ErrReadOnly)) // true
The library supports both to-one and to-many relationships:
type User struct {
ID string `jsonapi:"primary,users"`
Name string `jsonapi:"attr,name"`
Profile Profile `jsonapi:"relation,profile"` // To-one relationship
Posts []Post `jsonapi:"relation,posts"` // To-many relationship
}
type Profile struct {
ID string `jsonapi:"primary,profiles"`
Bio string `jsonapi:"attr,bio"`
UserID string `jsonapi:"attr,user_id"`
}
type Post struct {
ID string `jsonapi:"primary,posts"`
Title string `jsonapi:"attr,title"`
Content string `jsonapi:"attr,content"`
UserID string `jsonapi:"attr,user_id"`
}
You can include related resources in the response:
// Marshal with included related resources
data, err := jsonapi.Marshal(user, jsonapi.IncludeRelatedResources())
The library supports custom marshaling and unmarshaling through interfaces:
// Custom resource marshaling
type ResourceMarshaler interface {
MarshalJSONAPIResource(ctx context.Context) (Resource, error)
}
// Custom resource unmarshaling
type ResourceUnmarshaler interface {
UnmarshalJSONAPIResource(ctx context.Context, resource Resource) error
}
Other interfaces are available for more granular control:
// Marshaling interfaces
type LinksMarshaler interface {
MarshalJSONAPILinks(ctx context.Context) (map[string]Link, error)
}
type MetaMarshaler interface {
MarshalJSONAPIMeta(ctx context.Context) (map[string]interface{}, error)
}
type RelationshipLinksMarshaler interface {
MarshalJSONAPIRelationshipLinks(ctx context.Context, name string) (map[string]Link, error)
}
type RelationshipMetaMarshaler interface {
MarshalJSONAPIRelationshipMeta(ctx context.Context, name string) (map[string]interface{}, error)
}
// Unmarshaling interfaces
type LinksUnmarshaler interface {
UnmarshalJSONAPILinks(ctx context.Context, links map[string]Link) error
}
type MetaUnmarshaler interface {
UnmarshalJSONAPIMeta(ctx context.Context, meta map[string]interface{}) error
}
type RelationshipLinksUnmarshaler interface {
UnmarshalJSONAPIRelationshipLinks(ctx context.Context, name string, links map[string]Link) error
}
type RelationshipMetaUnmarshaler interface {
UnmarshalJSONAPIRelationshipMeta(ctx context.Context, name string, meta map[string]interface{}) error
}
The library provides iterator support for resource collections using Go's iter
package:
// Get a document with multiple resources
doc, err := jsonapi.MarshalDocument(context.Background(), users)
if err != nil {
panic(err)
}
// Iterate over resources in the primary data
for resource := range doc.Data.Iter() {
fmt.Printf("Resource ID: %s, Type: %s\n", resource.ID, resource.Type)
// Process attributes
for name, value := range resource.Attributes {
fmt.Printf("Attribute %s: %v\n", name, value)
}
// Process relationships
for name, rel := range resource.Relationships {
fmt.Printf("Relationship %s\n", name)
}
}
This makes it easy to process large collections of resources efficiently without having to manually check if the primary data contains a single resource or multiple resources.
The library includes a server
package that provides HTTP server utilities for building JSON:API compliant web services. It includes request context management, resource handlers, and routing utilities that simplify the creation of JSON:API endpoints following the specification.
The ResourceHandler
type provides HTTP handlers for different JSON:API resource operations:
type ResourceHandler struct {
Get http.Handler // Handler for GET requests to retrieve a single resource
Create http.Handler // Handler for POST requests to create new resources
Update http.Handler // Handler for PATCH requests to update existing resources
Delete http.Handler // Handler for DELETE requests to remove resources
Search http.Handler // Handler for GET requests to search/list resources
Relationship http.Handler // Handler for relationship-specific operations
}
The RelationshipHandler
type provides HTTP handlers for JSON:API relationship operations:
type RelationshipHandler struct {
Get http.Handler // Handler for GET requests to fetch relationship linkage
Add http.Handler // Handler for POST requests to add to to-many relationships
Update http.Handler // Handler for PATCH requests to update relationship linkage
Delete http.Handler // Handler for DELETE requests to remove from to-many relationships
}
The DefaultHandler
function creates a default HTTP handler with standard JSON:API routes configured:
func DefaultHandler(mux ResourceHandlerMux) http.Handler {
// Sets up all the conventional JSON:API endpoints including:
// - "GET /{type}" // Search/list resources of a type
// - "GET /{type}/{id}" // Get a single resource by ID
// - "POST /{type}" // Create a new resource
// - "PATCH /{type}/{id}" // Update an existing resource
// - "DELETE /{type}/{id}" // Delete a resource
// - "GET /{type}/{id}/relationships/{relationship}" // Get a resource's relationship
// - "GET /{type}/{id}/{related}" // Get related resources
// - "POST /{type}/{id}/relationships/{relationship}" // Add to a to-many relationship
// - "PATCH /{type}/{id}/relationships/{relationship}" // Update a relationship
// - "DELETE /{type}/{id}/relationships/{relationship}" // Remove from a to-many relationship
}
The RequestContext
type contains parsed information from an HTTP request that is relevant to JSON:API resource operations:
type RequestContext struct {
ResourceID string // The ID of the requested resource
ResourceType string // The type of the requested resource
Relationship string // The name of the requested relationship
FetchRelatedResources bool // Whether to fetch related resources instead of relationship linkage
}
package main
import (
"net/http"
"github.com/nisimpson/jsonapi"
"github.com/nisimpson/jsonapi/server"
)
func main() {
// Create resource handlers
usersHandler := server.ResourceHandler{
Get: http.HandlerFunc(getUserHandler),
Create: http.HandlerFunc(createUserHandler),
Search: http.HandlerFunc(searchUsersHandler),
// Add other handlers as needed
}
// Create a resource handler mux
mux := server.ResourceHandlerMux{
"users": usersHandler,
// Add other resource types as needed
}
// Create a default handler with standard JSON:API routes
handler := server.DefaultHandler(mux)
// Start the server
http.ListenAndServe(":8080", handler)
}
func getUserHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
requestContext, _ := server.GetRequestContext(ctx)
// Get the user by ID
user := getUser(requestContext.ResourceID)
// Marshal the user to JSON:API
doc, err := jsonapi.MarshalDocument(ctx, user)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Write the response
w.Header().Set("Content-Type", "application/vnd.api+json")
json.NewEncoder(w).Encode(doc)
}
// Implement other handlers similarly
The library provides a more convenient way to write JSON:API handlers using server.HandlerFunc
and server.Response
:
package main
import (
"net/http"
"github.com/nisimpson/jsonapi"
"github.com/nisimpson/jsonapi/server"
)
func main() {
// Create resource handlers using HandlerFunc
usersHandler := server.ResourceHandler{
Get: server.HandlerFunc(getUser),
Create: server.HandlerFunc(createUser),
Search: server.HandlerFunc(searchUsers),
}
// Create a resource handler mux
mux := server.ResourceHandlerMux{
"users": usersHandler,
}
// Create a default handler with standard JSON:API routes
handler := server.DefaultHandler(mux)
// Start the server
http.ListenAndServe(":8080", handler)
}
// Using HandlerFunc for cleaner handler implementation
func getUser(ctx *server.RequestContext, r *http.Request) server.Response {
// Get the user by ID
user, err := fetchUserFromDatabase(ctx.ResourceID)
if err != nil {
// Return a 404 response with error
return server.Response{
Status: http.StatusNotFound,
Body: jsonapi.NewErrorDocument(jsonapi.Error{
Status: "404",
Title: "Resource not found",
Detail: err.Error(),
}),
}
}
// Marshal the user to JSON:API
doc, err := jsonapi.MarshalDocument(r.Context(), user)
if err != nil {
// Return a 500 response with error
return server.Response{
Status: http.StatusInternalServerError,
Body: jsonapi.NewErrorDocument(jsonapi.Error{
Status: "500",
Title: "Internal server error",
Detail: err.Error(),
}),
}
}
// Return a structured response
return server.Response{
Status: http.StatusOK,
Header: http.Header{
"Cache-Control": []string{"max-age=3600"},
},
Body: doc,
}
}
// Example of handling errors with HandlerFunc
func createUser(ctx *server.RequestContext, r *http.Request) server.Response {
var user User
// Parse request body
if err := jsonapi.UnmarshalResourceInto(r.Context(), doc.Data, &user); err != nil {
return server.Response{
Status: http.StatusBadRequest,
Body: jsonapi.NewErrorDocument(jsonapi.Error{
Status: "400",
Title: "Invalid request body",
Detail: err.Error(),
}),
}
}
// Save user to database
if err := saveUserToDatabase(&user); err != nil {
return server.Response{
Status: http.StatusInternalServerError,
Body: jsonapi.NewErrorDocument(jsonapi.Error{
Status: "500",
Title: "Internal server error",
Detail: err.Error(),
}),
}
}
// Marshal the created user to JSON:API
responseDoc, err := jsonapi.MarshalDocument(r.Context(), user)
if err != nil {
return server.Response{
Status: http.StatusInternalServerError,
Body: jsonapi.NewErrorDocument(jsonapi.Error{
Status: "500",
Title: "Internal server error",
Detail: err.Error(),
}),
}
}
// Return a structured response with 201 Created status
return server.Response{
Status: http.StatusCreated,
Header: http.Header{
"Location": []string{"/users/" + user.ID},
},
Body: responseDoc,
}
}
The server.HandlerFunc
type provides several advantages:
- Automatic access to the parsed request context
- Structured response handling with status codes and headers
- Automatic error handling with proper JSON:API error formatting
- Cleaner handler implementation with less boilerplate code
The server.Response
struct allows you to specify:
- HTTP status code
- Custom HTTP headers
- JSON:API document body
The server.Write
and server.Error
functions are also available for more direct control over response writing:
// Write a JSON:API document response
server.Write(w, doc, http.StatusOK)
// Write a JSON:API error response
server.Error(w, err, http.StatusBadRequest)
The RequestContext
provides methods for parsing JSON:API query parameters:
// Get sparse fieldsets for a specific resource type
fields := requestContext.GetFields(r, "users")
// fields = ["name", "email"] for ?fields[users]=name,email
// Check if a relationship should be included
shouldIncludePosts := requestContext.ShouldInclude(r, "posts")
// true for ?include=posts,comments
The server package provides middleware for proper JSON:API content negotiation:
// Use content negotiation middleware
handler = server.UseContentNegotiation(handler)
This ensures proper handling of the Accept
and Content-Type
headers according to the JSON:API specification.
The library provides comprehensive error handling with detailed error messages:
// Create an error document
errorDoc := jsonapi.Document{
Errors: []jsonapi.Error{
{
Status: "404",
Title: "Resource not found",
Detail: "The requested resource could not be found",
},
},
}
// Marshal the error document
data, err := json.Marshal(errorDoc)
if err != nil {
panic(err)
}
// Write the error response
w.Header().Set("Content-Type", "application/vnd.api+json")
w.WriteHeader(http.StatusNotFound)
w.Write(data)
This project is licensed under the MIT License - see the LICENSE file for details.