jsonapi

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 25, 2025 License: MIT Imports: 10 Imported by: 0

README

JSON:API Library for Go

Test GoDoc Release

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.

Features

  • ✅ 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

Installation

go get github.com/nisimpson/jsonapi
Requirements
  • Go 1.24.4 or higher

Basic Usage

Marshaling
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"}}}
}
Unmarshaling
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)
}

Struct Tags

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
}
Tag Format
  • primary,type: Marks a field as the primary ID field and specifies the resource type
  • attr,name[,omitempty][,readonly]: Marks a field as an attribute with optional flags
  • relation,name[,omitempty][,readonly]: Marks a field as a relationship with optional flags
  • -: Ignores the field during marshaling/unmarshaling
Tag Options
  • omitempty: Omits the field during marshaling if it has a zero value
  • readonly: Tags the field as read-only (see below for details)
Read-Only Fields

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

Relationships

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

Custom Marshaling/Unmarshaling

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
}

Iterator Support

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.

HTTP Server Support

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.

Resource Handlers

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
}
Relationship Handlers

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
}
Default HTTP Routing

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
}
Request Context

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
}
Example Server Setup
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
Using server.Response and server.HandlerFunc

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:

  1. Automatic access to the parsed request context
  2. Structured response handling with status codes and headers
  3. Automatic error handling with proper JSON:API error formatting
  4. 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)

Request Parameter Parsing

The RequestContext provides methods for parsing JSON:API query parameters:

Sparse Fieldsets
// Get sparse fieldsets for a specific resource type
fields := requestContext.GetFields(r, "users")
// fields = ["name", "email"] for ?fields[users]=name,email
Includes
// Check if a relationship should be included
shouldIncludePosts := requestContext.ShouldInclude(r, "posts")
// true for ?include=posts,comments
Content Negotiation

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.

Error Handling

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)

License

This project is licensed under the MIT License - see the LICENSE file for details.

Documentation

Overview

Example

Example demonstrates marshaling and then unmarshaling (round-trip)

package main

import (
	"fmt"

	"github.com/nisimpson/jsonapi"
)

// Example structs
type User struct {
	ID    string `jsonapi:"primary,users"`
	Name  string `jsonapi:"attr,name"`
	Email string `jsonapi:"attr,email,omitempty"`
}

func main() {
	originalUser := User{
		ID:    "42",
		Name:  "Alice Smith",
		Email: "alice@example.com",
	}

	// Marshal to JSON:API
	marshaledData, _ := jsonapi.Marshal(originalUser)

	// Unmarshal back to struct
	var roundTripUser User
	err := jsonapi.Unmarshal(marshaledData, &roundTripUser)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
	} else {
		fmt.Printf("Round-trip User: ID=%s, Name=%s, Email=%s\n",
			roundTripUser.ID, roundTripUser.Name, roundTripUser.Email)
		fmt.Printf("Round-trip successful: %t\n",
			originalUser.ID == roundTripUser.ID &&
				originalUser.Name == roundTripUser.Name &&
				originalUser.Email == roundTripUser.Email)
	}
}
Output:

Round-trip User: ID=42, Name=Alice Smith, Email=alice@example.com
Round-trip successful: true
Example (UnmarshalDocument)

Example_unmarshalDocument demonstrates unmarshaling a JSON:API document directly

package main

import (
	"encoding/json"
	"fmt"

	"github.com/nisimpson/jsonapi"
)

func main() {
	jsonData := `{
		"data": {
			"type": "users",
			"id": "1",
			"attributes": {
				"name": "John Doe",
				"email": "john@example.com"
			}
		}
	}`

	doc := jsonapi.Document{}
	err := json.Unmarshal([]byte(jsonData), &doc)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
	} else {
		resource, ok := doc.Data.One()
		if ok {
			fmt.Printf("Document Resource: ID=%s, Type=%s\n", resource.ID, resource.Type)
			fmt.Printf("Name: %s\n", resource.Attributes["name"])
			fmt.Printf("Email: %s\n", resource.Attributes["email"])
		}
	}
}
Output:

Document Resource: ID=1, Type=users
Name: John Doe
Email: john@example.com

Index

Examples

Constants

View Source
const (
	// StructTagName is the name of the struct tag used for JSON:API field definitions.
	// Example:
	//
	// 	`jsonapi:"primary,users"`
	StructTagName = "jsonapi"

	// TagValuePrimary indicates a field contains the primary resource ID and type.
	// Format: `jsonapi:"primary,resource-type"`
	// Example:
	//
	// 	`jsonapi:"primary,users"`
	TagValuePrimary = "primary"

	// TagValueAttribute indicates a field should be marshaled as a JSON:API attribute.
	// Format: `jsonapi:"attr,attribute-name[,omitempty]"`
	// Example:
	//
	// 	`jsonapi:"attr,name"` or `jsonapi:"attr,email,omitempty"`
	TagValueAttribute = "attr"

	// TagValueRelationship indicates a field should be marshaled as a JSON:API relationship.
	// Format: `jsonapi:"relation,relationship-name[,omitempty]"`
	// Example:
	//
	// 	`jsonapi:"relation,posts"` or `jsonapi:"relation,profile,omitempty"`
	TagValueRelationship = "relation"

	// TagOptionOmitEmpty is a tag option that causes empty/zero values to be omitted
	// during marshaling. Can be used with both attributes and relationships.
	// Example:
	//
	// 	`jsonapi:"attr,email,omitempty"`
	TagOptionOmitEmpty = "omitempty"

	// TagOptionReadOnly is a tag option that prevents a field from being unmarshaled
	// unless the [PermitReadOnly] option is used. Read-only fields are still marshaled normally.
	// Can be used with both attributes and relationships.
	// Example:
	//
	// 	`jsonapi:"attr,created_at,readonly"` or `jsonapi:"relation,author,readonly"`
	TagOptionReadOnly = "readonly"

	// TagValueIgnore causes a field to be ignored during marshaling and unmarshaling.
	// Format: `jsonapi:"-"`
	TagValueIgnore = "-"
)

Struct tag constants for JSON:API field definitions. These constants define the tag name and tag values used in struct field tags to control JSON:API marshaling and unmarshaling behavior.

Variables

View Source
var (
	// ErrReadOnly is returned when attempts to [Unmarshal] a [Document] with
	// readonly attributes occur.
	ErrReadOnly = errors.New("read-only")
)

Functions

func DocumentLinks(links map[string]Link) func(*MarshalOptions)

DocumentLinks returns a function that modifies MarshalOptions to add links to the JSON:API document. The provided links map will be set as the top-level links object in the resulting document. This can be used to add pagination, self-referential, or related resource links to the document.

func DocumentMeta

func DocumentMeta(meta map[string]interface{}) func(*MarshalOptions)

DocumentMeta returns a function that modifies MarshalOptions to add metadata to the JSON:API document. The provided meta map will be set as the top-level meta object in the resulting document. This is useful for adding custom metadata like pagination info or document-level statistics.

func IncludeRelatedResources

func IncludeRelatedResources() func(*MarshalOptions)

IncludeRelatedResources instructs the Marshaler to add any related resources found within the document's primary data to the included array.

func Marshal

func Marshal(out interface{}, opts ...func(*MarshalOptions)) ([]byte, error)

Marshal marshals a Go struct into a JSON:API Resource using the default context.

Example (Custom)

ExampleMarshalCustom demonstrates custom marshaling using the MarshalJSONAPIResource interface

package main

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/nisimpson/jsonapi"
)

// Custom marshaler example
type CustomUser struct {
	ID   string
	Name string
}

func (u CustomUser) MarshalJSONAPIResource(ctx context.Context) (jsonapi.Resource, error) {
	return jsonapi.Resource{
		Type: "users",
		ID:   u.ID,
		Attributes: map[string]interface{}{
			"name":         u.Name,
			"custom_field": "custom_value",
		},
	}, nil
}

func main() {
	customUser := CustomUser{
		ID:   "1",
		Name: "John Doe",
	}

	data, _ := jsonapi.Marshal(customUser, jsonapi.WithMarshaler(func(out interface{}) ([]byte, error) {
		return json.MarshalIndent(out, "", "  ")
	}))
	fmt.Println(string(data))
}
Output:

{
  "data": {
    "id": "1",
    "type": "users",
    "attributes": {
      "custom_field": "custom_value",
      "name": "John Doe"
    }
  }
}
Example (MultipleResources)

ExampleMarshalMultipleResources demonstrates marshaling multiple resources to JSON:API format

package main

import (
	"encoding/json"
	"fmt"

	"github.com/nisimpson/jsonapi"
)

// Example structs
type User struct {
	ID    string `jsonapi:"primary,users"`
	Name  string `jsonapi:"attr,name"`
	Email string `jsonapi:"attr,email,omitempty"`
}

func main() {
	users := []User{
		{ID: "1", Name: "John Doe", Email: "john@example.com"},
		{ID: "2", Name: "Jane Doe", Email: "jane@example.com"},
	}

	data, _ := jsonapi.Marshal(users, jsonapi.WithMarshaler(func(out interface{}) ([]byte, error) {
		return json.MarshalIndent(out, "", "  ")
	}))
	fmt.Println(string(data))
}
Output:

{
  "data": [
    {
      "id": "1",
      "type": "users",
      "attributes": {
        "email": "john@example.com",
        "name": "John Doe"
      }
    },
    {
      "id": "2",
      "type": "users",
      "attributes": {
        "email": "jane@example.com",
        "name": "Jane Doe"
      }
    }
  ]
}
Example (Relationships)

ExampleMarshalRelationships demonstrates marshaling relationships with included resources

package main

import (
	"encoding/json"
	"fmt"

	"github.com/nisimpson/jsonapi"
)

type Post struct {
	ID    string `jsonapi:"primary,posts"`
	Title string `jsonapi:"attr,title"`
	Body  string `jsonapi:"attr,body"`
}

type UserWithPosts struct {
	ID    string `jsonapi:"primary,users"`
	Name  string `jsonapi:"attr,name"`
	Posts []Post `jsonapi:"relation,posts"`
}

func main() {
	userWithPosts := UserWithPosts{
		ID:   "1",
		Name: "John Doe",
		Posts: []Post{
			{ID: "1", Title: "First Post", Body: "Content 1"},
			{ID: "2", Title: "Second Post", Body: "Content 2"},
		},
	}

	data, _ := jsonapi.Marshal(userWithPosts,
		jsonapi.IncludeRelatedResources(),
		jsonapi.WithMarshaler(func(out interface{}) ([]byte, error) {
			return json.MarshalIndent(out, "", "  ")
		}))
	fmt.Println(string(data))
}
Output:

{
  "data": {
    "id": "1",
    "type": "users",
    "attributes": {
      "name": "John Doe"
    },
    "relationships": {
      "posts": {
        "data": [
          {
            "id": "1",
            "type": "posts"
          },
          {
            "id": "2",
            "type": "posts"
          }
        ]
      }
    }
  },
  "included": [
    {
      "id": "1",
      "type": "posts",
      "attributes": {
        "body": "Content 1",
        "title": "First Post"
      }
    },
    {
      "id": "2",
      "type": "posts",
      "attributes": {
        "body": "Content 2",
        "title": "Second Post"
      }
    }
  ]
}
Example (SingleResource)

ExampleMarshalSingleResource demonstrates marshaling a single resource to JSON:API format

package main

import (
	"encoding/json"
	"fmt"

	"github.com/nisimpson/jsonapi"
)

// Example structs
type User struct {
	ID    string `jsonapi:"primary,users"`
	Name  string `jsonapi:"attr,name"`
	Email string `jsonapi:"attr,email,omitempty"`
}

func main() {
	user := User{
		ID:    "1",
		Name:  "John Doe",
		Email: "john@example.com",
	}

	data, _ := jsonapi.Marshal(user, jsonapi.WithMarshaler(func(out interface{}) ([]byte, error) {
		return json.MarshalIndent(out, "", "  ")
	}))
	fmt.Println(string(data))
}
Output:

{
  "data": {
    "id": "1",
    "type": "users",
    "attributes": {
      "email": "john@example.com",
      "name": "John Doe"
    }
  }
}

func MarshalWithContext

func MarshalWithContext(ctx context.Context, out interface{}, opts ...func(*MarshalOptions)) ([]byte, error)

MarshalWithContext marshals a Go struct into a JSON:API Resource with a provided context.

func PermitReadOnly added in v0.2.0

func PermitReadOnly(enabled bool) func(*UnmarshalOptions)

PermitReadOnly returns an option that toggles unmarshaling of read-only fields. When disabled, any attempts to Unmarshal documents with read-only fields will return an error wrapping ErrReadOnly.

func PopulateFromIncluded

func PopulateFromIncluded() func(*UnmarshalOptions)

PopulateFromIncluded instructs the Unmarshaler to populate relationship fields from the included array when available.

func SparseFieldsets

func SparseFieldsets(resourceType string, fields []string) func(*MarshalOptions)

SparseFieldsets returns a function that modifies MarshalOptions to apply sparse fieldsets to resources. The function takes a resourceType string to identify which resources to modify and a fields slice containing the field names to include in the output. When applied, this function will filter both the primary data and included resources to only include the specified fields for resources matching the given type.

func StrictMode

func StrictMode() func(*UnmarshalOptions)

StrictMode enables strict mode unmarshaling which returns errors for invalid JSON:API structure.

func Unmarshal

func Unmarshal(data []byte, out interface{}, opts ...func(*UnmarshalOptions)) error

Unmarshal unmarshals JSON:API data into a Go struct using the default context.

Example (Custom)

ExampleUnmarshalCustom demonstrates custom unmarshaling using the UnmarshalJSONAPIResource interface

package main

import (
	"context"
	"fmt"

	"github.com/nisimpson/jsonapi"
)

// Custom unmarshaler example
type CustomUnmarshalUser struct {
	ID          string
	Name        string
	CustomField string
}

func (u *CustomUnmarshalUser) UnmarshalJSONAPIResource(ctx context.Context, resource jsonapi.Resource) error {
	u.ID = resource.ID
	if name, ok := resource.Attributes["name"].(string); ok {
		u.Name = name
	}
	if customField, ok := resource.Attributes["custom_field"].(string); ok {
		u.CustomField = customField
	}
	return nil
}

func main() {
	customJsonData := `{
		"data": {
			"type": "users",
			"id": "1",
			"attributes": {
				"name": "John Doe",
				"custom_field": "custom_value"
			}
		}
	}`

	var customUnmarshaledUser CustomUnmarshalUser
	err := jsonapi.Unmarshal([]byte(customJsonData), &customUnmarshaledUser)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
	} else {
		fmt.Printf("Custom User: ID=%s, Name=%s, CustomField=%s\n",
			customUnmarshaledUser.ID, customUnmarshaledUser.Name, customUnmarshaledUser.CustomField)
	}
}
Output:

Custom User: ID=1, Name=John Doe, CustomField=custom_value
Example (MultipleResources)

ExampleUnmarshalMultipleResources demonstrates unmarshaling multiple resources from JSON:API format

package main

import (
	"fmt"

	"github.com/nisimpson/jsonapi"
)

// Example structs
type User struct {
	ID    string `jsonapi:"primary,users"`
	Name  string `jsonapi:"attr,name"`
	Email string `jsonapi:"attr,email,omitempty"`
}

func main() {
	multipleJsonData := `{
		"data": [
			{
				"type": "users",
				"id": "1",
				"attributes": {
					"name": "John Doe",
					"email": "john@example.com"
				}
			},
			{
				"type": "users",
				"id": "2",
				"attributes": {
					"name": "Jane Doe",
					"email": "jane@example.com"
				}
			}
		]
	}`

	var unmarshaledUsers []User
	err := jsonapi.Unmarshal([]byte(multipleJsonData), &unmarshaledUsers)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
	} else {
		fmt.Printf("Number of users: %d\n", len(unmarshaledUsers))
		for i, user := range unmarshaledUsers {
			fmt.Printf("User %d: ID=%s, Name=%s, Email=%s\n",
				i+1, user.ID, user.Name, user.Email)
		}
	}
}
Output:

Number of users: 2
User 1: ID=1, Name=John Doe, Email=john@example.com
User 2: ID=2, Name=Jane Doe, Email=jane@example.com
Example (NestedStruct)

ExampleUnmarshalNestedStruct demonstrates unmarshaling nested struct attributes

package main

import (
	"fmt"

	"github.com/nisimpson/jsonapi"
)

type Address struct {
	Street string `json:"street"`
	City   string `json:"city"`
}

type UserWithAddress struct {
	ID      string  `jsonapi:"primary,users"`
	Name    string  `jsonapi:"attr,name"`
	Address Address `jsonapi:"attr,address"`
}

func main() {
	nestedJsonData := `{
		"data": {
			"type": "users",
			"id": "1",
			"attributes": {
				"name": "John Doe",
				"address": {
					"street": "123 Main St",
					"city": "Anytown"
				}
			}
		}
	}`

	var userWithAddressUnmarshaled UserWithAddress
	err := jsonapi.Unmarshal([]byte(nestedJsonData), &userWithAddressUnmarshaled)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
	} else {
		fmt.Printf("User: ID=%s, Name=%s\n", userWithAddressUnmarshaled.ID, userWithAddressUnmarshaled.Name)
		fmt.Printf("Address: Street=%s, City=%s\n",
			userWithAddressUnmarshaled.Address.Street, userWithAddressUnmarshaled.Address.City)
	}
}
Output:

User: ID=1, Name=John Doe
Address: Street=123 Main St, City=Anytown
Example (Relationships)

ExampleUnmarshalRelationships demonstrates unmarshaling relationships with included resources

package main

import (
	"fmt"

	"github.com/nisimpson/jsonapi"
)

type Post struct {
	ID    string `jsonapi:"primary,posts"`
	Title string `jsonapi:"attr,title"`
	Body  string `jsonapi:"attr,body"`
}

type UserWithPosts struct {
	ID    string `jsonapi:"primary,users"`
	Name  string `jsonapi:"attr,name"`
	Posts []Post `jsonapi:"relation,posts"`
}

func main() {
	relationshipJsonData := `{
		"data": {
			"type": "users",
			"id": "1",
			"attributes": {
				"name": "John Doe"
			},
			"relationships": {
				"posts": {
					"data": [
						{"type": "posts", "id": "1"},
						{"type": "posts", "id": "2"}
					]
				}
			}
		},
		"included": [
			{
				"type": "posts",
				"id": "1",
				"attributes": {
					"title": "First Post",
					"body": "Content 1"
				}
			},
			{
				"type": "posts",
				"id": "2",
				"attributes": {
					"title": "Second Post",
					"body": "Content 2"
				}
			}
		]
	}`

	var userWithPostsUnmarshaled UserWithPosts
	err := jsonapi.Unmarshal([]byte(relationshipJsonData), &userWithPostsUnmarshaled, jsonapi.PopulateFromIncluded())
	if err != nil {
		fmt.Printf("Error: %v\n", err)
	} else {
		fmt.Printf("User: ID=%s, Name=%s\n", userWithPostsUnmarshaled.ID, userWithPostsUnmarshaled.Name)
		fmt.Printf("Number of posts: %d\n", len(userWithPostsUnmarshaled.Posts))
		for i, post := range userWithPostsUnmarshaled.Posts {
			fmt.Printf("Post %d: ID=%s, Title=%s, Body=%s\n",
				i+1, post.ID, post.Title, post.Body)
		}
	}
}
Output:

User: ID=1, Name=John Doe
Number of posts: 2
Post 1: ID=1, Title=First Post, Body=Content 1
Post 2: ID=2, Title=Second Post, Body=Content 2
Example (SingleResource)

ExampleUnmarshalSingleResource demonstrates unmarshaling a single resource from JSON:API format

package main

import (
	"fmt"

	"github.com/nisimpson/jsonapi"
)

// Example structs
type User struct {
	ID    string `jsonapi:"primary,users"`
	Name  string `jsonapi:"attr,name"`
	Email string `jsonapi:"attr,email,omitempty"`
}

func main() {
	jsonData := `{
		"data": {
			"type": "users",
			"id": "1",
			"attributes": {
				"name": "John Doe",
				"email": "john@example.com"
			}
		}
	}`

	var unmarshaledUser User
	err := jsonapi.Unmarshal([]byte(jsonData), &unmarshaledUser)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
	} else {
		fmt.Printf("Unmarshaled User: ID=%s, Name=%s, Email=%s\n",
			unmarshaledUser.ID, unmarshaledUser.Name, unmarshaledUser.Email)
	}
}
Output:

Unmarshaled User: ID=1, Name=John Doe, Email=john@example.com

func UnmarshalDocument

func UnmarshalDocument(ctx context.Context, doc *Document, out interface{}, opts ...func(*UnmarshalOptions)) error

UnmarshalDocument unmarshals a Document into the target struct.

func UnmarshalWithContext

func UnmarshalWithContext(ctx context.Context, data []byte, out interface{}, opts ...func(*UnmarshalOptions)) error

UnmarshalWithContext unmarshals JSON:API data into a Go struct with a provided context.

func WithMarshaler

func WithMarshaler(fn func(interface{}) ([]byte, error)) func(*MarshalOptions)

WithMarshaler uses a custom JSON marshaler to serialize documents.

func WithUnmarshaler

func WithUnmarshaler(fn func([]byte, interface{}) error) func(*UnmarshalOptions)

WithUnmarshaler uses a custom JSON unmarshaler to deserialize documents.

Types

type Document

type Document struct {
	Meta     map[string]interface{} `json:"meta,omitempty"`
	Data     PrimaryData            `json:"data,omitempty"`
	Errors   []Error                `json:"errors,omitempty"`
	Links    map[string]Link        `json:"links,omitempty"`
	Included []Resource             `json:"included,omitempty"`
}

Document represents the top-level JSON:API document structure.

func MarshalDocument

func MarshalDocument(ctx context.Context, out interface{}, opts ...func(*MarshalOptions)) (*Document, error)

MarshalDocument converts a Go value into a JSON:API Document structure. It accepts a context for injection of request-scoped values in marshaling operations. Optional marshaling options can be provided to customize the marshaling behavior. It returns a Document pointer and any error encountered during marshaling. If the input is nil or a nil pointer, it returns an error.

This function is the core marshaling function used by Marshal and MarshalWithContext. It can be used directly when you need access to the Document structure before serialization.

func NewErrorDocument added in v0.1.3

func NewErrorDocument(err error) *Document

NewErrorDocument creates a new Document to represent errors in a JSON:API compliant format. This function handles different types of errors and converts them into the appropriate Document structure.

  • When the provided error wraps or is of type Error, the function will use that error directly in the document
  • If the error wraps or is of type MultiError, all of the contained errors will be included in the document.

For any other error type, the function creates a generic error entry using the error's message as the detail field. This ensures that even standard Go errors can be represented in the JSON:API format.

If a nil error is provided, the function creates a document with a generic "Unknown error" message. This prevents returning empty error documents.

type Error

type Error struct {
	ID     string                 `json:"id,omitempty"`
	Status string                 `json:"status"`
	Code   string                 `json:"code"`
	Title  string                 `json:"title"`
	Detail string                 `json:"detail"`
	Source map[string]interface{} `json:"source,omitempty"`
	Links  map[string]interface{} `json:"links,omitempty"`
}

Error represents a JSON:API error object.

func (Error) Error

func (e Error) Error() string

Error returns a string representation of the error. The returned string will include the title, detail, and code if they are available. If only the title and detail are available, it returns them formatted as "title: detail". If only the detail is available, it returns just the detail string.

type Link struct {
	Href string                 `json:"href,omitempty"`
	Meta map[string]interface{} `json:"meta,omitempty"`
}

Link represents a JSON:API link object.

func (Link) MarshalJSON

func (l Link) MarshalJSON() ([]byte, error)

func (*Link) UnmarshalJSON

func (l *Link) UnmarshalJSON(data []byte) error

type LinksMarshaler

type LinksMarshaler interface {
	MarshalJSONAPILinks(ctx context.Context) (map[string]Link, error)
}

LinksMarshaler allows types to provide custom links marshaling.

type LinksUnmarshaler

type LinksUnmarshaler interface {
	UnmarshalJSONAPILinks(ctx context.Context, links map[string]Link) error
}

LinksUnmarshaler allows types to provide custom links unmarshaling.

type MarshalOptions

type MarshalOptions struct {
	// contains filtered or unexported fields
}

MarshalOptions contains options for marshaling operations.

type MetaMarshaler

type MetaMarshaler interface {
	MarshalJSONAPIMeta(ctx context.Context) (map[string]interface{}, error)
}

MetaMarshaler allows types to provide custom meta marshaling.

type MetaUnmarshaler

type MetaUnmarshaler interface {
	UnmarshalJSONAPIMeta(ctx context.Context, meta map[string]interface{}) error
}

MetaUnmarshaler allows types to provide custom meta unmarshaling.

type MultiError

type MultiError []Error

MultiError represents a collection of JSON:API errors that can be combined into a single error. It implements the error interface and provides a way to handle multiple errors as one.

func (MultiError) Error

func (me MultiError) Error() string

Error returns a string representation of multiple errors combined into one. If the MultiError is empty, it panics with "multi error is empty". If the MultiError contains only one error, it returns that error's string representation. For multiple errors, it joins them together using errors.Join and returns the combined string.

type PrimaryData

type PrimaryData struct {
	// contains filtered or unexported fields
}

PrimaryData represents the primary data in a JSON:API document. It can be a single resource, multiple resources, or null.

func MultiResource

func MultiResource(resources ...Resource) PrimaryData

MultiResource creates primary data with multiple resources.

func NullResource

func NullResource() PrimaryData

NullResource creates null primary data.

func SingleResource

func SingleResource(resource Resource) PrimaryData

SingleResource creates primary data with a single resource.

func (*PrimaryData) Iter

func (pd *PrimaryData) Iter() iter.Seq[*Resource]

Iter returns an iterator over the resources.

func (PrimaryData) Many

func (pd PrimaryData) Many() ([]Resource, bool)

Many returns the resource slice and true if data contains multiple resources.

func (PrimaryData) MarshalJSON

func (pd PrimaryData) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler for PrimaryData.

func (PrimaryData) Null

func (pd PrimaryData) Null() bool

Null returns true if the data is null.

func (PrimaryData) One

func (pd PrimaryData) One() (Resource, bool)

One returns the single resource and true if data contains one resource.

func (*PrimaryData) UnmarshalJSON

func (pd *PrimaryData) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler for PrimaryData.

type Relationship

type Relationship struct {
	Meta  map[string]interface{} `json:"meta,omitempty"`
	Links map[string]Link        `json:"links,omitempty"`
	Data  PrimaryData            `json:"data,omitempty"`
}

Relationship represents a JSON:API relationship object.

type RelationshipLinksMarshaler

type RelationshipLinksMarshaler interface {
	MarshalJSONAPIRelationshipLinks(ctx context.Context, name string) (map[string]Link, error)
}

RelationshipLinksMarshaler allows types to provide custom relationship links marshaling.

type RelationshipLinksUnmarshaler

type RelationshipLinksUnmarshaler interface {
	UnmarshalJSONAPIRelationshipLinks(ctx context.Context, name string, links map[string]Link) error
}

RelationshipLinksUnmarshaler allows types to provide custom relationship links unmarshaling.

type RelationshipMetaMarshaler

type RelationshipMetaMarshaler interface {
	MarshalJSONAPIRelationshipMeta(ctx context.Context, name string) (map[string]interface{}, error)
}

RelationshipMetaMarshaler allows types to provide custom relationship meta marshaling.

type RelationshipMetaUnmarshaler

type RelationshipMetaUnmarshaler interface {
	UnmarshalJSONAPIRelationshipMeta(ctx context.Context, name string, meta map[string]interface{}) error
}

RelationshipMetaUnmarshaler allows types to provide custom relationship meta unmarshaling.

type Resource

type Resource struct {
	ID            string                  `json:"id"`
	Type          string                  `json:"type"`
	Meta          map[string]interface{}  `json:"meta,omitempty"`
	Attributes    map[string]interface{}  `json:"attributes,omitempty"`
	Relationships map[string]Relationship `json:"relationships,omitempty"`
	Links         map[string]Link         `json:"links,omitempty"`
}

Resource represents a JSON:API resource object.

func (*Resource) ApplySparseFieldsets

func (r *Resource) ApplySparseFieldsets(fields []string)

ApplySparseFieldsets filters the resource's attributes to only include the specified fields. If fields is empty, all attributes are retained. Otherwise, only attributes whose names match one of the provided fields are kept, and all other attributes are removed.

func (Resource) Ref

func (r Resource) Ref() Resource

Ref returns a resource reference (only ID and Type, no attributes/relationships).

type ResourceMarshaler

type ResourceMarshaler interface {
	MarshalJSONAPIResource(ctx context.Context) (Resource, error)
}

ResourceMarshaler allows types to provide custom JSON:API resource marshaling.

type ResourceUnmarshaler

type ResourceUnmarshaler interface {
	UnmarshalJSONAPIResource(ctx context.Context, resource Resource) error
}

ResourceUnmarshaler allows types to provide custom JSON:API resource unmarshaling.

type UnmarshalOptions

type UnmarshalOptions struct {
	// contains filtered or unexported fields
}

UnmarshalOptions contains options for unmarshaling operations.

Directories

Path Synopsis
Package server provides HTTP server utilities for building JSON:API compliant web services.
Package server provides HTTP server utilities for building JSON:API compliant web services.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL