jsonapi

package module
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: Jul 31, 2024 License: Apache-2.0 Imports: 11 Imported by: 3

README

Go Reference test golangci-lint GitHub release (latest SemVer)

jsonapi

Package jsonapi implements a marshaler/unmarshaler for JSON:API v1.0.

Version

This package is in production use at DataDog and should be considered stable and production ready.

We follow semver and are reserving the v1.0.0 release until:

  • JSON:API v1.1 is released and we evaluate any breaking changes needed (unlikely)
  • Community adoption and feedback are evaluated
  • Continued internal use lets us gain more experience with the package

Quickstart

Take a look at the go reference for more examples and detailed usage information.

Marshaling

jsonapi.Marshal

type Article struct {
    ID    string `jsonapi:"primary,articles"`
    Title string `jsonapi:"attribute" json:"title"`
}

a := Article{ID: "1", Title:"Hello World"}

b, err := jsonapi.Marshal(&a)
if err != nil {
    // ...
}

fmt.Println("%s", string(b))
// {
//     "data": {
//         "id": "1",
//         "type": "articles",
//         "attributes": {
//             "title": "Hello World"
//         }
//     }
// }

Unmarshaling

jsonapi.Unmarshal

body := `{"data":{"id":"1","type":"articles","attributes":{"title":"Hello World"}}}`

type Article struct {
    ID    string `jsonapi:"primary,articles"`
    Title string `jsonapi:"attribute" json:"title"`
}

var a Article
if err := jsonapi.Unmarshal([]byte(body), &a); err != nil {
    // ...
}

fmt.Prints("%s, %s", a.ID, a.Title)
// "1", "Hello World"

Reference

The following information is well documented in the go reference. This section is included for a high-level overview of the features available.

Struct Tags

Like encoding/json jsonapi is primarily controlled by the presence of a struct tag jsonapi. The standard json tag is still used for naming and omitempty.

Tag Usage Description Alias
primary jsonapi:"primary,{type},{omitempty}" Defines the identification field. Including omitempty allows for empty IDs (used for server-side id generation) N/A
attribute jsonapi:"attribute" Defines an attribute. attr
relationship jsonapi:"relationship" Defines a relationship. rel
meta jsonapi:"meta" Defines a meta object. N/A

Functional Options

Both jsonapi.Marshal and jsonapi.Unmarshal take functional options.

Option Supports
jsonapi.MarshalOption meta, json:api, includes, document links, sparse fieldsets, name validation
jsonapi.UnmarshalOption meta, document links, name validation

Non-String Identifiers

Identification MUST be represented as a string regardless of the actual type in Go. To support non-string types for the primary field you can implement optional interfaces.

You can implement the following on the parent types (that contain non-string fields):

Context Interface
Marshal jsonapi.MarshalIdentifier
Unmarshal jsonapi.UnmarshalIdentifier

You can implement the following on the field types themselves if they are not already implemented.

Context Interface
Marshal fmt.Stringer
Marshal encoding.TextMarshaler
Unmarshal encoding.TextUnmarshaler

Order of Operations

Marshaling
  1. Use MarshalIdentifier if it is implemented on the parent type
  2. Use the value directly if it is a string
  3. Use fmt.Stringer if it is implemented
  4. Use encoding.TextMarshaler if it is implemented
  5. Fail
Unmarshaling
  1. Use UnmarshalIdentifier if it is implemented on the parent type
  2. Use encoding.TextUnmarshaler if it is implemented
  3. Use the value directly if it is a string
  4. Fail

Links are supported via two interfaces and the Link type. To include links you must implement one or both of the following interfaces.

Type Interface
Resource Object Link Linkable
Resource Object Related Resource Link LinkableRelation

Alternatives

google/jsonapi

  • exposes an API that looks/feels a lot different than encoding/json
  • has quite a few bugs w/ complex types in attributes
  • doesn't provide easy access to top-level fields like meta
  • allows users to generate invalid JSON:API
  • not actively maintained

manyminds/api2go/jsonapi

  • has required interfaces
  • interfaces for includes/relationships are hard to understand and verbose to implement
  • allows users to generate invalid JSON:API
  • part of a broader api2go framework ecosystem

Documentation

Overview

Package jsonapi implements encoding and decoding of JSON:API as defined in https://jsonapi.org/format/.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrMarshalInvalidPrimaryField indicates that the id (primary) fields was invalid.
	ErrMarshalInvalidPrimaryField = errors.New("primary/id field must be a string or implement fmt.Stringer or in a struct which implements MarshalIdentifier")

	// ErrUnmarshalInvalidPrimaryField indicates that the id (primary) fields was invalid.
	ErrUnmarshalInvalidPrimaryField = errors.New("primary/id field must be a string or in a struct which implements UnmarshalIdentifer")

	// ErrUnmarshalDuplicatePrimaryField indicates that the id (primary) field is duplicated in a struct.
	ErrUnmarshalDuplicatePrimaryField = errors.New("there must be only one `jsonapi:\"primary\"` field to Unmarshal")

	// ErrMissingPrimaryField indicates that the id (primary) field is not identified.
	ErrMissingPrimaryField = errors.New("primary/id field must labeled with `jsonapi:\"primary,{type}\"`")

	// ErrEmptyPrimaryField indicates that the id (primary) field is identified but empty.
	ErrEmptyPrimaryField = errors.New("the `jsonapi:\"primary\"` field value must not be empty")

	// ErrMissingLinkFields indicates that a LinkObject is not valid.
	ErrMissingLinkFields = errors.New("at least one of Links.Self or Links.Related must be set to a nonempty string or *LinkObject")

	// ErrEmptyDataObject indicates that a primary or relationship data member is incorrectly represented by an empty JSON object {}
	ErrEmptyDataObject = errors.New("resource \"data\" members may not be represented by an empty object {}")

	// ErrDocumentMissingRequiredMembers indicates that a document does not have at least one required top-level member
	ErrDocumentMissingRequiredMembers = errors.New("document is missing required top-level members; must have one of: \"data\", \"meta\", \"errors\"")

	// ErrRelationshipMissingRequiredMembers indicates that a relationship does not have at least one required member
	ErrRelationshipMissingRequiredMembers = errors.New("relationship is missing required top-level members; must have one of: \"data\", \"meta\", \"links\"")

	// ErrErrorUnmarshalingNotImplemented indicates that an attempt was made to unmarshal an error document
	ErrErrorUnmarshalingNotImplemented = errors.New("error unmarshaling is not implemented")
)

Functions

func Marshal

func Marshal(v any, opts ...MarshalOption) (b []byte, err error)

Marshal returns the json:api encoding of v. If v is type *Error or []*Error only the errors will be marshaled.

Example
package main

import (
	"fmt"

	"github.com/DataDog/jsonapi"
)

func main() {
	type Article struct {
		ID    string `jsonapi:"primary,articles"`
		Title string `jsonapi:"attribute" json:"title"`
	}

	a := Article{ID: "1", Title: "Hello World"}

	b, err := jsonapi.Marshal(&a)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%s", string(b))
}
Output:

{"data":{"id":"1","type":"articles","attributes":{"title":"Hello World"}}}
Example (Meta)
package main

import (
	"fmt"

	"github.com/DataDog/jsonapi"
)

func main() {
	type ArticleMeta struct {
		Views int `json:"views"`
	}
	type Article struct {
		ID    string       `jsonapi:"primary,articles"`
		Title string       `jsonapi:"attribute" json:"title"`
		Meta  *ArticleMeta `jsonapi:"meta"`
	}

	a := Article{ID: "1", Title: "Hello World", Meta: &ArticleMeta{Views: 10}}
	m := map[string]any{"foo": "bar"}

	b, err := jsonapi.Marshal(&a, jsonapi.MarshalMeta(m))
	if err != nil {
		panic(err)
	}

	fmt.Printf("%s", string(b))
}
Output:

{"data":{"id":"1","type":"articles","attributes":{"title":"Hello World"},"meta":{"views":10}},"meta":{"foo":"bar"}}
Example (Relationships)
package main

import (
	"fmt"

	"github.com/DataDog/jsonapi"
)

type Author struct {
	ID   string `jsonapi:"primary,author"`
	Name string `jsonapi:"attribute" json:"name"`
}

type Comment struct {
	ID     string  `jsonapi:"primary,comments"`
	Body   string  `jsonapi:"attribute" json:"comment"`
	Author *Author `jsonapi:"relationship"`
}

func (c *Comment) LinkRelation(relation string) *jsonapi.Link {
	return &jsonapi.Link{
		Self:    fmt.Sprintf("http://example.com/comments/%s/relationships/%s", c.ID, relation),
		Related: fmt.Sprintf("http://example.com/comments/%s/%s", c.ID, relation),
	}
}

type Article struct {
	ID       string     `jsonapi:"primary,articles"`
	Title    string     `jsonapi:"attribute" json:"title"`
	Author   *Author    `jsonapi:"relationship" json:"author,omitempty"`
	Comments []*Comment `jsonapi:"relationship" json:"comments,omitempty"`
}

func (a *Article) LinkRelation(relation string) *jsonapi.Link {
	return &jsonapi.Link{
		Self:    fmt.Sprintf("http://example.com/articles/%s/relationships/%s", a.ID, relation),
		Related: fmt.Sprintf("http://example.com/articles/%s/%s", a.ID, relation),
	}
}

func main() {
	authorA := &Author{ID: "AA", Name: "Cool Author"}
	authorB := &Author{ID: "AB", Name: "Cool Commenter"}
	authorC := &Author{ID: "AC", Name: "Neat Commenter"}
	commentA := &Comment{ID: "CA", Body: "Very cool", Author: authorB}
	commentB := &Comment{ID: "CB", Body: "Super neat", Author: authorC}
	article := Article{
		ID:       "1",
		Title:    "Hello World",
		Author:   authorA,
		Comments: []*Comment{commentA, commentB},
	}

	b, err := jsonapi.Marshal(&article)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%s", string(b))
}
Output:

{"data":{"id":"1","type":"articles","attributes":{"title":"Hello World"},"relationships":{"author":{"data":{"id":"AA","type":"author"},"links":{"self":"http://example.com/articles/1/relationships/author","related":"http://example.com/articles/1/author"}},"comments":{"data":[{"id":"CA","type":"comments"},{"id":"CB","type":"comments"}],"links":{"self":"http://example.com/articles/1/relationships/comments","related":"http://example.com/articles/1/comments"}}}}}
Example (Slice)
package main

import (
	"fmt"

	"github.com/DataDog/jsonapi"
)

func main() {
	type Article struct {
		ID    string `jsonapi:"primary,articles"`
		Title string `jsonapi:"attribute" json:"title"`
	}

	a := []*Article{
		{ID: "1", Title: "Hello World"},
		{ID: "2", Title: "Hello Again"},
	}

	b, err := jsonapi.Marshal(&a)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%s", string(b))
}
Output:

{"data":[{"id":"1","type":"articles","attributes":{"title":"Hello World"}},{"id":"2","type":"articles","attributes":{"title":"Hello Again"}}]}

func Status added in v0.4.0

func Status(s int) *int

Status provides a helper for setting an Error.Status value.

func Unmarshal

func Unmarshal(data []byte, v any, opts ...UnmarshalOption) (err error)

Unmarshal parses the json:api encoded data and stores the result in the value pointed to by v. If v is nil or not a pointer, Unmarshal returns an error.

Example
package main

import (
	"fmt"

	"github.com/DataDog/jsonapi"
)

func main() {
	body := `{"data":{"id":"1","type":"articles","attributes":{"title":"Hello World"}}}`

	type Article struct {
		ID    string `jsonapi:"primary,articles"`
		Title string `jsonapi:"attribute" json:"title"`
	}

	var a Article
	err := jsonapi.Unmarshal([]byte(body), &a)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v", &a)
}
Output:

&{ID:1 Title:Hello World}
Example (Slice)
package main

import (
	"fmt"

	"github.com/DataDog/jsonapi"
)

func main() {
	body := `{"data":[{"id":"1","type":"articles","attributes":{"title":"Hello World"}},{"id":"2","type":"articles","attributes":{"title":"Hello Again"}}]}`

	type Article struct {
		ID    string `jsonapi:"primary,articles"`
		Title string `jsonapi:"attribute" json:"title"`
	}

	var a []*Article
	err := jsonapi.Unmarshal([]byte(body), &a)
	if err != nil {
		panic(err)
	}

	fmt.Printf("%+v %+v", a[0], a[1])
}
Output:

&{ID:1 Title:Hello World} &{ID:2 Title:Hello Again}

Types

type Error

type Error struct {
	ID     string       `json:"id,omitempty"`
	Links  *ErrorLink   `json:"links,omitempty"`
	Status *int         `json:"status,omitempty"`
	Code   string       `json:"code,omitempty"`
	Title  string       `json:"title,omitempty"`
	Detail string       `json:"detail,omitempty"`
	Source *ErrorSource `json:"source,omitempty"`
	Meta   any          `json:"meta,omitempty"`
}

Error represents a JSON:API error object as defined by https://jsonapi.org/format/1.1/#error-objects.

func (*Error) Error added in v0.3.0

func (e *Error) Error() string

Error implements the error interface.

func (*Error) MarshalJSON added in v0.4.0

func (e *Error) MarshalJSON() ([]byte, error)

MarshalJSON implements the json.Marshaler interface.

func (*Error) UnmarshalJSON added in v0.7.2

func (e *Error) UnmarshalJSON(data []byte) error

UnmarshalJSON implements the json.Unmarshaler interface.

type ErrorLink struct {
	About any `json:"about,omitempty"`
	Type  any `json:"type,omitempty"`
}

ErrorLink represents a JSON:API error links object as defined by https://jsonapi.org/format/1.1/#error-objects.

type ErrorSource

type ErrorSource struct {
	Pointer   string `json:"pointer,omitempty"`
	Parameter string `json:"parameter,omitempty"`
	Header    string `json:"header,omitempty"`
}

ErrorSource represents a JSON:API Error.Source as defined by https://jsonapi.org/format/1.1/#error-objects.

type Link struct {
	Self    any `json:"self,omitempty"`
	Related any `json:"related,omitempty"`

	First    string `json:"first,omitempty"`
	Last     string `json:"last,omitempty"`
	Next     string `json:"next,omitempty"`
	Previous string `json:"previous,omitempty"`
}

Link is the top-level links object as defined by https://jsonapi.org/format/1.0/#document-top-level. First|Last|Next|Previous are provided to support pagination as defined by https://jsonapi.org/format/1.0/#fetching-pagination.

type LinkObject

type LinkObject struct {
	Href string `json:"href,omitempty"`
	Meta any    `json:"meta,omitempty"`
}

LinkObject is a links object as defined by https://jsonapi.org/format/1.0/#document-links

type Linkable

type Linkable interface {
	Link() *Link
}

Linkable can be implemented to marshal resource object links as defined by https://jsonapi.org/format/1.0/#document-resource-object-links.

type LinkableRelation

type LinkableRelation interface {
	LinkRelation(relation string) *Link
}

LinkableRelation can be implemented to marshal resource object related resource links as defined by https://jsonapi.org/format/1.0/#document-resource-object-related-resource-links.

type MarshalIdentifier

type MarshalIdentifier interface {
	MarshalID() string
}

MarshalIdentifier can be optionally implemented to control marshaling of the primary field to a string.

The order of operations for marshaling the primary field is:

  1. Use MarshalIdentifier if it is implemented
  2. Use the value directly if it is a string
  3. Use fmt.Stringer if it is implemented
  4. Fail

type MarshalOption

type MarshalOption func(m *Marshaler)

MarshalOption allows for configuration of Marshaling.

func MarshalClientMode added in v0.2.0

func MarshalClientMode() MarshalOption

MarshalClientMode enables client mode which skips validation only relevant for servers writing JSON:API responses.

func MarshalFields

func MarshalFields(query url.Values) MarshalOption

MarshalFields supports sparse fieldsets as defined by https://jsonapi.org/format/1.0/#fetching-sparse-fieldsets. The input is a url.Values and if given only the fields included in `fields[type]=a,b` are included in the response.

func MarshalInclude

func MarshalInclude(v ...any) MarshalOption

MarshalInclude includes the json:api encoding of v within Document.Included creating a compound document as defined by https://jsonapi.org/format/#document-compound-documents.

func MarshalJSONAPI

func MarshalJSONAPI(meta any) MarshalOption

MarshalJSONAPI includes the given meta (must be a map or struct) as Document.JSONAPI.Meta when marshaling. This also enables writing Document.JSONAPI.Version.

func MarshalLinks(l *Link) MarshalOption

MarshalLinks includes the given links as Document.Links when marshaling.

func MarshalMeta

func MarshalMeta(meta any) MarshalOption

MarshalMeta includes the given meta (must be a map or struct) as Document.Meta when marshaling.

func MarshalSetNameValidation added in v0.8.0

func MarshalSetNameValidation(mode MemberNameValidationMode) MarshalOption

MarshalSetNameValidation enables a given level of document member name validation.

type Marshaler

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

Marshaler is configured internally via MarshalOption's passed to Marshal. It's used to configure the Marshaling by including optional fields like Meta or JSONAPI.

type MemberNameValidationError added in v0.5.0

type MemberNameValidationError struct {
	MemberName string
}

MemberNameValidationError indicates that a document member name failed a validation step.

func (*MemberNameValidationError) Error added in v0.5.0

func (e *MemberNameValidationError) Error() string

Error implements the error interface.

type MemberNameValidationMode added in v0.8.0

type MemberNameValidationMode int

MemberNameValidationMode controls how document member names are checked for correctness.

const (
	// DefaultValidation verifies that member names are valid according to the spec in
	// https://jsonapi.org/format/#document-member-names.
	//
	// Note that this validation mode allows for non-URL-safe member names.
	DefaultValidation MemberNameValidationMode = iota

	// DisableValidation turns off member name validation for convenience or performance-saving
	// reasons.
	//
	// Note that this validation mode allows member names that do NOT conform to the JSON:API spec.
	DisableValidation

	// StrictValidation verifies that member names are valid according to the spec in
	// https://jsonapi.org/format/#document-member-names, and follow recommendations from
	// https://jsonapi.org/recommendations/#naming.
	//
	// Note that these names are always URL-safe.
	StrictValidation
)

type PartialLinkageError

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

PartialLinkageError indicates that an incomplete relationship chain was encountered.

func (*PartialLinkageError) Error

func (e *PartialLinkageError) Error() string

Error implements the error interface.

type TagError

type TagError struct {
	TagName string
	Field   string
	Reason  string
}

TagError indicates that an invalid struct tag was encountered.

func (*TagError) Error

func (e *TagError) Error() string

Error implements the error interface.

type TypeError

type TypeError struct {
	Actual   string
	Expected []string
}

TypeError indicates that an unexpected type was encountered.

func (*TypeError) Error

func (e *TypeError) Error() string

Error implements the error interface.

type UnmarshalIdentifier

type UnmarshalIdentifier interface {
	UnmarshalID(id string) error
}

UnmarshalIdentifier can be optionally implemented to control unmarshaling of the primary field from a string.

The order of operations for unmarshaling the primary field is:

  1. Use UnmarshalIdentifier if it is implemented
  2. Use the value directly if it is a string
  3. Fail

type UnmarshalOption

type UnmarshalOption func(m *Unmarshaler)

UnmarshalOption allows for configuration of Unmarshaling.

func UnmarshalLinks(link *Link) UnmarshalOption

UnmarshalLinks copies the Document.Links into the given link.

func UnmarshalMeta

func UnmarshalMeta(meta any) UnmarshalOption

UnmarshalMeta decodes Document.Meta into the given interface when unmarshaling.

Example
package main

import (
	"fmt"

	"github.com/DataDog/jsonapi"
)

func main() {
	body := `{"data":{"id":"1","type":"articles","attributes":{"title":"Hello World"},"meta":{"views":10}},"meta":{"foo":"bar"}}`

	type ArticleMeta struct {
		Views int `json:"views"`
	}
	type Article struct {
		ID    string       `jsonapi:"primary,articles"`
		Title string       `jsonapi:"attribute" json:"title"`
		Meta  *ArticleMeta `jsonapi:"meta"`
	}

	var (
		a Article
		m map[string]any
	)
	err := jsonapi.Unmarshal([]byte(body), &a, jsonapi.UnmarshalMeta(&m))
	if err != nil {
		panic(err)
	}

	fmt.Printf("%s %s %+v %+v", a.ID, a.Title, a.Meta, m)
}
Output:

1 Hello World &{Views:10} map[foo:bar]

func UnmarshalSetNameValidation added in v0.8.0

func UnmarshalSetNameValidation(mode MemberNameValidationMode) UnmarshalOption

UnmarshalSetNameValidation enables a given level of document member name validation.

type Unmarshaler

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

Unmarshaler is configured internally via UnmarshalOption's passed to Unmarshal. It's used to configure the Unmarshaling by decoding optional fields like Meta.

Directories

Path Synopsis
internal
is
Package is provides test assertion utilities.
Package is provides test assertion utilities.

Jump to

Keyboard shortcuts

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