Skip to content

Commit e03a6d4

Browse files
committed
Add support to Marshal slices of nested objects
1 parent 9333e5c commit e03a6d4

File tree

3 files changed

+105
-2
lines changed

3 files changed

+105
-2
lines changed

models_test.go

+10
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,16 @@ type Company struct {
190190
FoundedAt time.Time `jsonapi:"attr,founded-at,iso8601"`
191191
}
192192

193+
type CompanyOmitEmpty struct {
194+
ID string `jsonapi:"primary,companies"`
195+
Name string `jsonapi:"attr,name,omitempty"`
196+
Boss Employee `jsonapi:"attr,boss,omitempty"`
197+
Manager *Employee `jsonapi:"attr,manager,omitempty"`
198+
Teams []Team `jsonapi:"attr,teams,omitempty"`
199+
People []*People `jsonapi:"attr,people,omitempty"`
200+
FoundedAt time.Time `jsonapi:"attr,founded-at,iso8601,omitempty"`
201+
}
202+
193203
type People struct {
194204
Name string `jsonapi:"attr,name"`
195205
Age int `jsonapi:"attr,age"`

response.go

+24-2
Original file line numberDiff line numberDiff line change
@@ -401,14 +401,36 @@ func visitModelNode(model interface{}, included *map[string]*Node,
401401
continue
402402
}
403403

404-
if fieldValue.Type().Kind() == reflect.Struct || (fieldValue.Type().Kind() == reflect.Pointer && fieldValue.Elem().Kind() == reflect.Struct) {
404+
isStruct := fieldValue.Type().Kind() == reflect.Struct
405+
isPointerToStruct := fieldValue.Type().Kind() == reflect.Pointer && fieldValue.Elem().Kind() == reflect.Struct
406+
isSliceOfStruct := fieldValue.Type().Kind() == reflect.Slice && fieldValue.Type().Elem().Kind() == reflect.Struct
407+
isSliceOfPointerToStruct := fieldValue.Type().Kind() == reflect.Slice && fieldValue.Type().Elem().Kind() == reflect.Pointer && fieldValue.Type().Elem().Elem().Kind() == reflect.Struct
408+
409+
if isSliceOfStruct || isSliceOfPointerToStruct {
410+
if fieldValue.Len() == 0 && omitEmpty {
411+
continue
412+
}
413+
// Nested slice of object attributes
414+
manyNested, err := visitModelNodeRelationships(fieldValue, nil, false)
415+
if err != nil {
416+
er = fmt.Errorf("failed to marshal slice of nested attribute %q: %w", args[1], err)
417+
break
418+
}
419+
nestedNodes := make([]any, len(manyNested.Data))
420+
for i, n := range manyNested.Data {
421+
nestedNodes[i] = n.Attributes
422+
}
423+
node.Attributes[args[1]] = nestedNodes
424+
} else if isStruct || isPointerToStruct {
425+
// Nested object attribute
405426
nested, err := visitModelNode(fieldValue.Interface(), nil, false)
406427
if err != nil {
407428
er = fmt.Errorf("failed to marshal nested attribute %q: %w", args[1], err)
408429
break
409430
}
410431
node.Attributes[args[1]] = nested.Attributes
411432
} else {
433+
// Primative attribute
412434
strAttr, ok := fieldValue.Interface().(string)
413435
if ok {
414436
node.Attributes[args[1]] = strAttr
@@ -626,7 +648,7 @@ func visitModelNodeRelationships(models reflect.Value, included *map[string]*Nod
626648

627649
for i := 0; i < models.Len(); i++ {
628650
model := models.Index(i)
629-
if !model.IsValid() || model.IsNil() {
651+
if !model.IsValid() || (model.Kind() == reflect.Pointer && model.IsNil()) {
630652
return nil, ErrUnexpectedNil
631653
}
632654

response_test.go

+71
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,14 @@ func TestMarshalObjectAttribute(t *testing.T) {
694694
Firstname: "Dave",
695695
HiredAt: &now,
696696
},
697+
Teams: []Team{
698+
{Name: "Team 1"},
699+
{Name: "Team-2"},
700+
},
701+
People: []*People{
702+
{Name: "Person-1"},
703+
{Name: "Person-2"},
704+
},
697705
}
698706

699707
out := bytes.NewBuffer(nil)
@@ -734,6 +742,69 @@ func TestMarshalObjectAttribute(t *testing.T) {
734742
if manager["firstname"] != "Dave" {
735743
t.Fatalf("Expected manager.firstname to be \"Dave\", got %v", manager)
736744
}
745+
746+
people, ok := data.Attributes["people"].([]interface{})
747+
if !ok {
748+
t.Fatalf("Expected people attribute, got %v", data.Attributes)
749+
}
750+
if len(people) != 2 {
751+
t.Fatalf("Expected 2 people, got %v", people)
752+
}
753+
754+
teams, ok := data.Attributes["teams"].([]interface{})
755+
if !ok {
756+
t.Fatalf("Expected teams attribute, got %v", data.Attributes)
757+
}
758+
if len(teams) != 2 {
759+
t.Fatalf("Expected 2 teams, got %v", teams)
760+
}
761+
}
762+
763+
func TestMarshalObjectAttributeWithEmptyNested(t *testing.T) {
764+
testModel := &CompanyOmitEmpty{
765+
ID: "5",
766+
Name: "test",
767+
Boss: Employee{},
768+
Manager: nil,
769+
Teams: []Team{},
770+
People: nil,
771+
}
772+
773+
out := bytes.NewBuffer(nil)
774+
if err := MarshalPayload(out, testModel); err != nil {
775+
t.Fatal(err)
776+
}
777+
778+
resp := new(OnePayload)
779+
if err := json.NewDecoder(out).Decode(resp); err != nil {
780+
t.Fatal(err)
781+
}
782+
783+
data := resp.Data
784+
785+
if data.Attributes == nil {
786+
t.Fatalf("Expected attributes")
787+
}
788+
789+
_, ok := data.Attributes["boss"].(map[string]interface{})
790+
if ok {
791+
t.Fatalf("Expected omitted boss attribute, got %v", data.Attributes)
792+
}
793+
794+
_, ok = data.Attributes["manager"].(map[string]interface{})
795+
if ok {
796+
t.Fatalf("Expected omitted manager attribute, got %v", data.Attributes)
797+
}
798+
799+
_, ok = data.Attributes["people"].([]interface{})
800+
if ok {
801+
t.Fatalf("Expected omitted people attribute, got %v", data.Attributes)
802+
}
803+
804+
_, ok = data.Attributes["teams"].([]interface{})
805+
if ok {
806+
t.Fatalf("Expected omitted teams attribute, got %v", data.Attributes)
807+
}
737808
}
738809

739810
func TestOmitsZeroTimes(t *testing.T) {

0 commit comments

Comments
 (0)