Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions jsonapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,3 +385,27 @@ type MarshalIdentifier interface {
type UnmarshalIdentifier interface {
UnmarshalID(id string) error
}

// MarshalType can be optionally implemented to control marshaling of the type field.
//
// The order of operations for marshaling the type field is:
//
// 1. Use MarshalType if it is implemented
// 2. Use the value from the jsonapi tag on the primary field
// 3. Fail
type MarshalType interface {
MarshalType() string
}

// UnmarshalType can be optionally implemented to control unmarshaling of the type field from a string.
// Since the type is not typically set as a field on the object, this is an opportunity to return an error
// if the passed in type from the payload is unexpected. This allows customization of the expected type field.
//
// The order of operations for checking the type field is:
//
// 1. Use UnmarshalType if it is implemented, fail if it returns an error
// 2. Compare against the type provided in the jsonapi tag on the primary field
// 3. Fail
type UnmarshalType interface {
UnmarshalType(jsonapiType string) error
}
18 changes: 18 additions & 0 deletions jsonapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ var (
articleLinkedInvalidSelfMeta = ArticleLinkedInvalidSelfMeta{ID: "1"}
articleOmitTitleFull = ArticleOmitTitle{ID: "1"}
articleOmitTitlePartial = ArticleOmitTitle{ID: "1", Subtitle: "A"}
articleACustomType = ArticleTypeOverride{ID: "1", Title: "A"}
articleAIntID = ArticleIntID{ID: 1, Title: "A"}
articleBIntID = ArticleIntID{ID: 2, Title: "B"}
articlesIntIDABPtr = []*ArticleIntID{&articleAIntID, &articleBIntID}
Expand Down Expand Up @@ -150,6 +151,7 @@ var (
emptySingleBody = `{"data":{}}`
emptyManyBody = `{"data":[]}`
articleABody = `{"data":{"type":"articles","id":"1","attributes":{"title":"A"}}}`
articleACustomTypeBody = `{"data":{"type":"customarticles","id":"1","attributes":{"title":"A"}}}`
articleANoIDBody = `{"data":{"type":"articles","attributes":{"title":"A"}}}`
articleAInvalidTypeBody = `{"data":{"type":"not-articles","id":"1","attributes":{"title":"A"}}}`
articleOmitTitleFullBody = `{"data":{"type":"articles","id":"1"}}`
Expand Down Expand Up @@ -352,6 +354,22 @@ type ArticleIntID struct {
Title string `jsonapi:"attribute" json:"title"`
}

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

func (a *ArticleTypeOverride) MarshalType() string {
return "customarticles"
}

func (a *ArticleTypeOverride) UnmarshalType(jsonapiType string) error {
if jsonapiType != "customarticles" {
return fmt.Errorf("invalid type: %s", jsonapiType)
}
return nil
}

func (a *ArticleIntID) MarshalID() string {
return fmt.Sprintf("%d", a.ID)
}
Expand Down
3 changes: 3 additions & 0 deletions marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,9 @@ func (d *document) makeResourceObject(v any, vt reflect.Type, m *Marshaler) (*re
switch tag.directive {
case primary:
ro.Type = tag.resourceType
if vmt, ok := v.(MarshalType); ok {
ro.Type = vmt.MarshalType()
}
if !isValidMemberName(ro.Type, m.memberNameValidationMode) {
// type names count as member names
return nil, &MemberNameValidationError{ro.Type}
Expand Down
5 changes: 5 additions & 0 deletions marshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ func TestMarshal(t *testing.T) {
given: []string{"a", "b"},
expect: "",
expectError: &TypeError{Actual: "string", Expected: []string{"struct"}},
}, {
description: "*ArticleMarshalType (MarshalType)",
given: &articleACustomType,
expect: articleACustomTypeBody,
expectError: nil,
}, {
description: "*ArticleIntID (MarshalIdentifier)",
given: &articleAIntID,
Expand Down
13 changes: 12 additions & 1 deletion unmarshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package jsonapi
import (
"encoding"
"encoding/json"
"errors"
"reflect"
)

Expand Down Expand Up @@ -232,7 +233,17 @@ func (ro *resourceObject) unmarshalFields(v any, rv reflect.Value, rt reflect.Ty
if setPrimary {
return ErrUnmarshalDuplicatePrimaryField
}
if ro.Type != jsonapiTag.resourceType {

if vut, ok := v.(UnmarshalType); ok {
err := vut.UnmarshalType(ro.Type)
if err != nil {
return errors.Join(
&TypeError{Actual: ro.Type, Expected: []string{jsonapiTag.resourceType}},
err,
)
}

} else if ro.Type != jsonapiTag.resourceType {
return &TypeError{Actual: ro.Type, Expected: []string{jsonapiTag.resourceType}}
}
if !isValidMemberName(ro.Type, m.memberNameValidationMode) {
Expand Down
10 changes: 10 additions & 0 deletions unmarshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,16 @@ func TestUnmarshal(t *testing.T) {
},
expect: []*Article{&articleA, &articleB},
expectError: nil,
}, {
description: "*ArticleUnmarshalType",
given: articleACustomTypeBody,
do: func(body []byte) (any, error) {
var a ArticleTypeOverride
err := Unmarshal(body, &a)
return &a, err
},
expect: &articleACustomType,
expectError: nil,
}, {
description: "*ArticleIntID",
given: articleABody,
Expand Down
Loading