jsonapi

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Oct 17, 2025 License: MIT Imports: 9 Imported by: 0

README

jsonapi

The jsonapi package is a utility for marshaling and unmarshaling Go structs to and from JSON:API v1.1 formatted JSON.

Features:

  • Struct tags define the mapping between struct fields and the JSON:API id, attributes, relationships and metadata.
  • Marshaling and unmarshaling behaviour can be customised by implementing the ResourceMarshaler and ResourceUnmarshaler interfaces, respectively.
  • Exposes an API similar to the standard encoding/json package.
  • Supports anonymous/embedded struct fields.

Planned feaures:

  • Strict mode that enforces JSON:API compliant output.
  • Marshaling and unmarshaling arrays of resources.
  • Marshaling and unmarshaling top-level JSON:API documents

Usage

Import with

import (
	"github.com/max-waters/jsonapi"
)

Two functions are exposed:

MarshalResource(a any) ([]byte, error)
UnmarshalResource(data []byte, a any) error

MarshalResource returns the JSON:API encoding of a, and UnmarshalResource parses the JSON:API-encoded bytes data and stores the result in the value pointed to by a.

Example

Go code:

type Article struct {
    ID       int    `jsonapi:"id,articles,string"`
    Title    string `jsonapi:"attr,title"`
    Author   int    `jsonapi:"rel,author,people,string"`
    Comments []int  `jsonapi:"rel,comments,comments,string"`
    Deleted  bool   `jsonapi:"meta,deleted"`
}

a := Article{
    ID:       1,
    Title:    "Hello World",
    Author:   2,
    Comments: []int{3, 4},
    Deleted:  false,
}

b, err := jsonapi.MarshalResource(&a)
if err != nil {
  // handle error
}
fmt.Println(string(b))

The resulting JSON:API:

{
  "type": "articles",
  "id": "1",
  "meta": {
    "deleted": false
  },
  "attributes": {
    "title": "Hello World"
  },
  "relationships": {
    "author": {
      "data": {
        "type": "people",
        "id": "2"
      }
    },
    "comments": {
      "data": [
        {
          "type": "comments",
          "id": "3"
        },
        {
          "type": "comments",
          "id": "4"
        }
      ]
    }
  }
}

Mapping structs to JSON:API

The mapping between struct fields and the JSON:API id, attributes, relationships and metadata is defined with struct tags. The marshal and unmarshal functions will look for these tags in the top-level fields of in the input struct, and those in any anonymous struct fields. The values of these fields are marshaled and unmarshaled into the appropriate location in the resulting JSON:API using the encoding/json package.

Note that if a struct field has no jsonapi tag, then it is assumed to be an attribute (see below) with the encoding/json default name. A struct field can be exlcuded from the mapping with the "ignore" tag, jsonapi:"-".

IDs

The id tag defines the resource's primary id:

`jsonapi:"id,{type},[options]"`

The tagged field's value is mapped to the resource's "id" field, and the {type} argument defines the content of the "type" field. The field value is marshaled and unmarshaled with the encoding/json package.

Note that the jsonapi package does not (currently) enforce the JSON:API requirement that the "id" field be a string. However, the string option will encode floating point or integer values as JSON strings, allowing them to be used as valid JSON:API identifiers.

The omitempty option will exclude zero-valued values from the resulting JSON, allowing for empty IDs (eg for server-side ID generation).

Example ID with string option

Struct tags:

type Article struct {
    ID int    `jsonapi:"id,articles,string"`
}

a := Article{
    ID: 1,
}

JSON:API:

{
  "type": "articles",
  "id": "1",
}
Example ID with omitempty option

Struct tags:

type Article struct {
    ID int `jsonapi:"id,articles,string,omitempty"`
}

a := Article{}

JSON:API:

{
  "type": "articles"
}
Attributes

An attribute is defined by providing either an attr tag, or no jsonapi tag at all:

`jsonapi:"attr,{name},[options]"`

The field's value will be mapped to an attribute with the key specified by {name}. If no jsonapi tag is defined, or the {name} argument is empty, then the encoding/json default is used instead, ie either the name defined in the json tag, or the declared field name if none is found. The field value is marshaled and unmarshaled with the encoding/json package.

The attr tag supports the string and omitempty options, which encode numeric values as JSON strings, and omit zero-valued fields, respectively.

Example Attributes

Struct tags:

type Copyright struct {
    Owner string    `json:"owner"`
    Date  time.Time `json:"date"`
}

type Article struct {
    Title    string     `json:"title"`
    Content  string     `jsonapi:"content,omitempty"`
    Copyright Copyright `jsonapi:"attr,copyright"`
}

a := Article{
    Title: "Hello World",
    Copyright: Copyright {
        Owner: "Publishing Ltd",
        Date:  time.Now()
    }
}

JSON:API:

{
  "attributes": {
    "title": "Hello World",
    "copyright": {
        "owner": "Publishing Ltd",
        "date": "2024-12-12T21:46:43.552855+11:00"
    }
  },
}
Relationships

The rel tag defines a relationship:

`jsonapi:"rel,{name},{type},[options]"`

Any field annotated with a rel tag will be mapped to a relationship with the key specified by {name}. If the {name} argument is empty, then the encoding/json default is used instead, ie either the name defined in the json tag, or the declared field name if none is found.

The field's declared type determines whether it maps to a to-one or a to-many relationship. Array and slices of anything except bytes (or pointers to these), will be mapped to a to-many relationship, and all other types are mapped to a to-one relationship. For to-one relationships, the field's value maps to the relationship's "id" field, and the {type} argument defines the "type" field. For to-many relationships, each element in the array or slice defines the "id" of a related resource. The IDs are marshaled and unmarshaled with the encoding/json package.

While the jsonapi package does not (currently) enforce the JSON:API requirement that the "id" field be a string, the string option will encode floating point or integer IDs as JSON strings, allowing them to be used as valid JSON:API identifiers. And the omitempty option will exclude relationships with zero-valued valued IDs from the resulting JSON.

Example To-One and To-Many Relationships with string option

Struct tags:

type Article struct {
    Author   int    `jsonapi:"rel,author,people,string"`
    Comments []int  `jsonapi:"rel,comments,comments,string"`
}

a := Article{
    Author:   2,
    Comments: []int{3, 4},
}

JSON:API:

{
  "relationships": {
    "author": {
      "data": {
        "type": "people",
        "id": "2"
      }
    },
    "comments": {
      "data": [
        {
          "type": "comments",
          "id": "3"
        },
        {
          "type": "comments",
          "id": "4"
        }
      ]
    }
  }
}
Example Relationship with omitempty option

Struct tags:

type Article struct {
    Author   int    `jsonapi:"rel,author,people,string"`
    Comments []int  `jsonapi:"rel,comments,comments,string"`
}

a := Article{
    Comments: []int{3, 4},
}

JSON:API:

{
  "relationships": {
    "comments": {
      "data": [
        {
          "type": "comments",
          "id": "3"
        },
        {
          "type": "comments",
          "id": "4"
        }
      ]
    }
  }
}
Metadata

The meta tag defines a metadata item:

`jsonapi:"meta,{name},[options]"`

The field's value will be mapped to a metadata item with the key specified by {name}. If the {name} argument is empty, then the encoding/json default is used instead, ie either the name defined in the json tag, or the declared field name if none is found. The field value is marshaled and unmarshaled with the encoding/json package.

The meta tag supports the string and omitempty options, which encode numeric values as JSON strings, and omit zero-valued fields, respectively.

The link tag defines a link:

`jsonapi:"link,{name},[options]"`

The field's value will be mapped to a link with the key specified by {name}. If the {name} argument is empty, then the encoding/json default is used instead, ie either the name defined in the json tag, or the declared field name if none is found. The field value is marshaled and unmarshaled with the encoding/json package.

Note that the jsonapi package does not (currently) enforce the requirement that all links be either valid URIs or link objects.

The link tag supports the string and omitempty options, which encode numeric values as JSON strings, and omit zero-valued fields, respectively.

Anonymous Struct Fields

Anonymous (ie, embedded) struct fields are "promoted" and treated as though their members are declared in their parent type:

type SessionAuthz struct {
    Editable  bool `jsonapi:"attr,editable"`
    Deletable bool `jsonapi:"attr,deletable"`
}

type Article struct {
    SessionAuthz
    Title string `jsonapi:"attr,title"`
}

a := Article{
    SessionAuthz: SessionAuthz{
        Editable: true,
        Deletable: false,
    },
    Title: "Hello World",
}

JSON:API:

{
  "attributes": {
    "deletable": false,
    "editable": true,
    "title": "Hello World"
  }
}

Names clashes are resolved with standard Go promotion rules, as used by the encoding/json package. If two or more attr, rel or meta fields have the same name, then a selection is made based on the fields' nesting depth, then the presence of a jsonapi tag, then the presence of a json tag. If no single preferred field is found, then all clashing fields are excluded from the marshaling and unmarshaling.

Customising Resource Marshaling and Unmarshaling

The jsonapi package provides two interfaces and a number of functional options to help with custom marshaling and unmarshaling.

ResourceMarshaler and ResourceUnmarshaler

These interfaces allow a type to marshal or unmarshal itself to or from JSON:API:

type ResourceMarshaler interface {
    MarshalJsonApiResource() ([]byte, error)
}

type ResourceUnmarshaler interface {
    UnmarshalJsonApiResource([]byte) error
}
Example ResourceMarshaler and ResourceUnmarshaler

In this example, the Article type formats the created attribute as an RFC3339 timestamp by creating an alias type, and then calling the jsonapi marshaling and unmarshaling functions:

type Article struct {
    ID      int
    Created time.Time
}

func (a *Article) MarshalJsonApiResource() ([]byte, error) {
    type alias struct {
        ID      int    `jsonapi:"id,articles,string"`
        Created string `jsonapi:"attr,created"`
    }

    b := alias{
        ID:      a.ID
        Created: a.Created.Format(time.RFC3339),
    }

    return jsonapi.MarshalResource(&b)
}

func (a *Article) UnmarshalJsonApiResource(data []byte) error {
    type alias struct {
        ID      int    `jsonapi:"id,articles,string"`
        Created string `jsonapi:"attr,created"`
    }

    b := alias{}

    if err := jsonapi.UnmarshalResource(data, &b); err != nil {
        return err
    }

    created, err := time.Parse(time.RFC3339, b.Created)
    if err != nil {
        return err
    }

    a.ID = b.ID
    a.Created = created
    return nil
}

The functional options WithResourceLinker and WithRelationshipLinker allow for resource and relationship links to be generated from struct tags as well as information not contained in the resource.

The WithResourceLinker option accepts a function with the signature func(a any, r jsonapi.ResourceIdentifier) (map[string]jsonapi.Link, error) that should return all links for the supplied resource and JSON:API id. The function will be called for every resource.

The WithRelationshipLinker option accepts a function with the signature func(r any, id jsonapi.ResourceIdentifier, rel string, toOne bool, data ...jsonapi.ResourceIdentifier) (map[string]jsonapi.Link, error) that should return all links for the supplied relationship on the supplied resource. The function will be called for every relationship on every resource.

Example WithResourceLinker and WithRelationshipLinker Function

In this example, self and related links are generated from the type names declared in the resource's struct tags and a pre-configured URL base:

func InitResourceLinker(urlBase string) jsonapi.ResourceLinker {
	return func(r any, id jsonapi.ResourceIdentifier) (map[string]jsonapi.Link, error) {
		return map[string]jsonapi.Link{
			"self": jsonapi.LinkUri{Uri: fmt.Sprintf("%s/%s/%s", urlBase, id.Type, id.Id)},
		}, nil
	}
}

func InitRelationshipLinker(urlBase string) jsonapi.RelationshipLinker {
	return func(r any, id jsonapi.ResourceIdentifier, rel string, toOne bool, data ...jsonapi.ResourceIdentifier) (map[string]jsonapi.Link, error) {
		return map[string]Link{
			"self":    jsonapi.LinkUri{Uri: fmt.Sprintf("%s/%s/%s/relationships/%s", urlBase, id.Type, id.Id, rel)},
			"related": jsonapi.LinkUri{Uri: fmt.Sprintf("%s/%s/%s/%s", urlBase, id.Type, id.Id, rel)},
		}, nil
	}
}

type Article struct {
    ID       int
    Comments []int
}

a := Article{
    Id: 4,
    Comments: []int{5}
}

urlBase := "https://example.com"

jsonapi.MarshalResource(a, 
  jsonapi.WithResourceLinker(InitResourceLinker(urlBase)),
  jsonapi.WithRelationshipLinker(InitRelationshipLinker(urlBase)),
)

JSON:API:

{
  "type": "articles",
  "id": "4",
  "relationships": {
    "comments": {
      "data": [ 
        { 
          "type": "comment", 
          "id": 5 
        } 
      ],
      "links": {
        "related": "https://example.com/articles/4/comments",
        "self": "https://example.com/articles/4/relationships/comments"
      }
    }
  },
  "links": {
    "self": "https://example.com/articles/4"
  }
}

Documentation

Overview

Package jsonapi marshals and unmarshals JSON:API v1.1 formatted JSON. The mapping between JSON:API values and Go values is defined with struct tags, which can be overridden with custom marshalling and unmarshaling functions.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func MarshalResource

func MarshalResource(a any, opts ...marshalResourceOpt) ([]byte, error)

MashalResource returns the JSON:API encoding of resource a.

If a is nil or not a valid Resource type (ie is neither a struct nor a ResourceMarshaler) then a ResourceTypeError is returned.

If a implements ResourceMarshaler, then its [ResourceMarshaler.MarshalJsonApiResource] function is called.

Otherwise, the encoding of each of a's struct fields is defined by the the "jsonapi" key in the field's tag. The following formats are accepted:

  • "id,<type-name>[,<opt1>[,<opt2>]]": field is encoded as the resource type and id.
  • "attr[,<name>[,<opt1>[,opt2]]]": field is be encoded as an attribute.
  • "rel,<name>,<type-name>[,<opt1>[,<opt2>]]": field is encoded as a related resource id.
  • "link[,<name>[,<opt1>[,opt2]]]": field is encoded as a link.
  • "meta[,<name>[,<opt1>[,opt2]]]": field is encoded as a metadata item.
  • "-": field is ignored.

The tag's first element specifies the field's destination in the resource, and must be either "id", "attr", "rel", "meta", "link" or "-". The "id" specification must be followed by a type, a "rel" specification must be followed by a name and type, and the "attr", "meta" and "link" specifications may be followed by a name. A field with no tag defaults to attr, and an empty or unspecified name will default to the field name.

The following options are supported:

  • "omitempty" specifies that the field should be omitted from the Resource if the field has an empty value, as in the encoding/json package.

  • "string" signals that a floating point, integer, or boolean type should be encoded as a string. This is useful for using int or UUID fields as resource IDs.

Some struct tag examples:

// Field appears as the `id` field, converted to a string.
// Additionally, a `type` field will be added with value `my-type`.
Field int `jsonapi:"id,my-type,string"`

// Field appears as an attribute with name `my-name`.
Field int `jsonapi:"attr,my-name,omitempty"`

// Field appears as an attribute with name `my-name`.
Field int `json:"my-name"`

// Field is excluded from the resource entirely:
Field int `jsonapi:"-"`

// Field appears as the "id" of a to-one relationship, with name `my-name`, and type `my-type`.
Field int `jsonapi:"attr,my-name,my-type,string"`

// Field elements appear as the "id" fields of a to-many relationship, with name `my-name`, and type `my-type`.
Field []int `jsonapi:"attr,my-name,my-type,string"`

// Field appears as an meta item with name `my-name`.
Field int `jsonapi:"meta,my-name"`

// Field appears as a link with name `my-name`.
Field int `jsonapi:"link,my-name"`

Embedded struct fields without a jsonapi tag are marshaled as if their inner exported fields were fields in the outer struct, subject to the same visibility rules defined in the `encoding/json` package. Embedded struct fields with a tag are treated as though they are not embedded.

func UnmarshalResource

func UnmarshalResource(data []byte, a any) error

UnmarshalResource parses the JSON:API-formatted resource data and stores the result in the value pointed to by a. If a is nil or not a pointer, an IllegalUnmarshalError is returned. If a does not point to a struct type or a ResourceUnmarshaler, a ResourceTypeError is returned.

If a implements ResourceUnmarshaler, then its [ResourceUnmarshaler.UnmarshalJsonApiResource] function is called.

Otherwise, a's struct fields are decoded using struct tag and embedding rules equivalent to those used by MarshalResource.

func WithRelationshipLinker

func WithRelationshipLinker(links RelationshipLinker) marshalResourceOpt

WithRelationshipLinker returns a resource marshaling option that will set links on relationships to those returned by the the supplied RelationshipLinker function.

func WithResourceLinker

func WithResourceLinker(links ResourceLinker) marshalResourceOpt

WithResourceLinker returns a resource marshaling option that will set resource links to those returned by the suppled ResourceLinker function.

Types

type FieldTypeError added in v0.3.0

type FieldTypeError struct {
	Field string
	Kind  reflect.Kind
}

A FieldTypeError describes a Go struct field of a type that cannot be marshaled to, or unmarshaled from, JSON:API.

func (*FieldTypeError) Error added in v0.3.0

func (e *FieldTypeError) Error() string

type IllegalUnmarshalError added in v0.3.0

type IllegalUnmarshalError struct {
	Value reflect.Value
}

An IllegalUnmarshalError describes an illegal input to UnmarshalResource (ie, not a pointer, or nil).

func (*IllegalUnmarshalError) Error added in v0.3.0

func (e *IllegalUnmarshalError) Error() string
type Link interface {
	// contains filtered or unexported methods
}

The Link interface represents a JSON:API link, ie either a uri reference or an object. It has a single unexported method and so cannot be implemented by code outside of this package.

type LinkObject

type LinkObject struct {
	Href        string         `json:"href,omitempty"`
	DescribedBy Link           `json:"described_by,omitempty"`
	Title       string         `json:"title,omitempty"`
	Type        string         `json:"type,omitempty"`
	HrefLang    []string       `json:"hreflang,omitempty"`
	Meta        map[string]any `json:"meta,omitempty"`
}

A LinkObject represents a JSON:API object link.

func (*LinkObject) UnmarshalJSON

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

type LinkUri

type LinkUri struct {
	Uri string
}

A LinkUri represents a JSON:API URI-reference link.

func (LinkUri) MarshalJSON

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

func (*LinkUri) UnmarshalJSON

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

type MarshalError added in v0.3.0

type MarshalError struct {
	Field string
	Err   error
}

A MarshalError describes an error returned by the underlying JSON marshaling library.

func (*MarshalError) Error added in v0.3.0

func (e *MarshalError) Error() string

type RelationshipLinker

type RelationshipLinker func(r any, id ResourceIdentifier, rel string, toOne bool, data ...ResourceIdentifier) (map[string]Link, error)

A RelationshipLinker function should return all links for the given relationship, on the given resource. Input r is the value passed to MarshalResource, id is the ResourceIdentifier extracted from r, rel is the relationship name, toOne indicates if the relationship is toOne or toMany, and data contains a ResourceIdentifier for each related resource. Can be used with WithRelationshipLinker to set links that are not present in the struct passed to MarshalResource.

Example:

func MyRelationshipLinker(r any, id ResourceIdentifier, rel string, toOne bool, data ...ResourceIdentifier) (map[string]Link, error) {
	return map[string]Link{
		"related": LinkUri{Uri: fmt.Sprintf("https://example.com/%s/%s/%s", id.Type, id.Id, rel)},
	}, nil
}

type ResourceIdentifier

type ResourceIdentifier struct {
	Type string
	Id   string
}

A ResourceIdentifier represents a JSON:API resource identifier.

type ResourceLinker

type ResourceLinker func(r any, id ResourceIdentifier) (map[string]Link, error)

A ResourceLinker function should return all links for the supplied resource. Input r is the value passed to MarshalResource, and id is the ResourceIdentifier extracted from r. Can be used with WithResourceLinker to set links that are not present in the struct passed to MarshalResource.

Example:

func MyResourceLinker(r any, id ResourceIdentifier) (map[string]Link, error) {
	return map[string]Link{
		"self": LinkUri{
			Uri: fmt.Sprintf("https://example.com/%s/%s", id.Type, id.Id),
		},
	}, nil
}

type ResourceMarshaler

type ResourceMarshaler interface {
	MarshalJsonApiResource() ([]byte, error)
}

ResourceMarshaler is the interface implemented by types that can marshal themselves into JSON:API-formatted JSON.

type ResourceTypeError added in v0.3.0

type ResourceTypeError struct {
	Type reflect.Type
}

A ResourceTypeError describes a Go type that cannot be be marshaled to, or unmarshaled from, a JSON:API resource (ie, not a struct, or does not implement ResourceMarshaler or ResourceUnmarshaler).

func (*ResourceTypeError) Error added in v0.3.0

func (e *ResourceTypeError) Error() string

type ResourceUnmarshaler

type ResourceUnmarshaler interface {
	UnmarshalJsonApiResource([]byte) error
}

ResourceUnmarshaler is the interface implemented by types that can unmarshal themselves from JSON:API-formatted JSON.

type SelfReferentialPointerError added in v0.3.0

type SelfReferentialPointerError struct {
	Value reflect.Value
}

A SelfReferentialPointerError describes a loop of pointers.

func (*SelfReferentialPointerError) Error added in v0.3.0

type TagError added in v0.3.0

type TagError struct {
	Field string
	Err   error
}

A TagError describes an unknown or incorrectly formatted jsonapi struct tag.

func (*TagError) Error added in v0.3.0

func (e *TagError) Error() string

type UnmarshalError added in v0.3.0

type UnmarshalError struct {
	Field string
	Err   error
}

An UnmarshalError describes an error returned by the underlying JSON unmarshaling library.

func (*UnmarshalError) Error added in v0.3.0

func (e *UnmarshalError) Error() string

Jump to

Keyboard shortcuts

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