Skip to content

Commit 9333e5c

Browse files
committed
Support nested objects within attributes when Marshaling
Unmarshaling an attribute that is a struct or struct pointer that is decorated with jsonapi tags has historically worked as expected, but, curiously, marshaling did not and required the use of `json` tags instead, making struct reuse difficult for both input and output object attributes. Now, you can specify a struct or struct pointer as an attribute and it will be marshaled into the correct keys. Note that this implemenation does not yet support slices of structs or struct pointers.
1 parent 1dc4f04 commit 9333e5c

File tree

3 files changed

+79
-9
lines changed

3 files changed

+79
-9
lines changed

models_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ type Company struct {
184184
ID string `jsonapi:"primary,companies"`
185185
Name string `jsonapi:"attr,name"`
186186
Boss Employee `jsonapi:"attr,boss"`
187+
Manager *Employee `jsonapi:"attr,manager"`
187188
Teams []Team `jsonapi:"attr,teams"`
188189
People []*People `jsonapi:"attr,people"`
189190
FoundedAt time.Time `jsonapi:"attr,founded-at,iso8601"`

response.go

+24-9
Original file line numberDiff line numberDiff line change
@@ -226,14 +226,20 @@ func visitModelNode(model interface{}, included *map[string]*Node,
226226
node := new(Node)
227227

228228
var er error
229+
var modelValue reflect.Value
230+
var modelType reflect.Type
229231
value := reflect.ValueOf(model)
230-
if value.IsNil() {
231-
return nil, nil
232+
if value.Type().Kind() == reflect.Pointer {
233+
if value.IsNil() {
234+
return nil, nil
235+
}
236+
modelValue = value.Elem()
237+
modelType = value.Type().Elem()
238+
} else {
239+
modelValue = value
240+
modelType = value.Type()
232241
}
233242

234-
modelValue := value.Elem()
235-
modelType := value.Type().Elem()
236-
237243
for i := 0; i < modelValue.NumField(); i++ {
238244
fieldValue := modelValue.Field(i)
239245
structField := modelValue.Type().Field(i)
@@ -395,11 +401,20 @@ func visitModelNode(model interface{}, included *map[string]*Node,
395401
continue
396402
}
397403

398-
strAttr, ok := fieldValue.Interface().(string)
399-
if ok {
400-
node.Attributes[args[1]] = strAttr
404+
if fieldValue.Type().Kind() == reflect.Struct || (fieldValue.Type().Kind() == reflect.Pointer && fieldValue.Elem().Kind() == reflect.Struct) {
405+
nested, err := visitModelNode(fieldValue.Interface(), nil, false)
406+
if err != nil {
407+
er = fmt.Errorf("failed to marshal nested attribute %q: %w", args[1], err)
408+
break
409+
}
410+
node.Attributes[args[1]] = nested.Attributes
401411
} else {
402-
node.Attributes[args[1]] = fieldValue.Interface()
412+
strAttr, ok := fieldValue.Interface().(string)
413+
if ok {
414+
node.Attributes[args[1]] = strAttr
415+
} else {
416+
node.Attributes[args[1]] = fieldValue.Interface()
417+
}
403418
}
404419
}
405420
} else if annotation == annotationRelation || annotation == annotationPolyRelation {

response_test.go

+54
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,60 @@ func TestSupportsAttributes(t *testing.T) {
682682
}
683683
}
684684

685+
func TestMarshalObjectAttribute(t *testing.T) {
686+
now := time.Now()
687+
testModel := &Company{
688+
ID: "5",
689+
Name: "test",
690+
Boss: Employee{
691+
HiredAt: &now,
692+
},
693+
Manager: &Employee{
694+
Firstname: "Dave",
695+
HiredAt: &now,
696+
},
697+
}
698+
699+
out := bytes.NewBuffer(nil)
700+
if err := MarshalPayload(out, testModel); err != nil {
701+
t.Fatal(err)
702+
}
703+
704+
resp := new(OnePayload)
705+
if err := json.NewDecoder(out).Decode(resp); err != nil {
706+
t.Fatal(err)
707+
}
708+
709+
data := resp.Data
710+
711+
if data.Attributes == nil {
712+
t.Fatalf("Expected attributes")
713+
}
714+
715+
boss, ok := data.Attributes["boss"].(map[string]interface{})
716+
if !ok {
717+
t.Fatalf("Expected boss attribute, got %v", data.Attributes)
718+
}
719+
720+
hiredAt, ok := boss["hired-at"]
721+
if !ok {
722+
t.Fatalf("Expected boss attribute to contain a \"hired-at\" property, got %v", boss)
723+
}
724+
725+
if hiredAt != now.UTC().Format(iso8601TimeFormat) {
726+
t.Fatalf("Expected hired-at to be %s, got %s", now.UTC().Format(iso8601TimeFormat), hiredAt)
727+
}
728+
729+
manager, ok := data.Attributes["manager"].(map[string]interface{})
730+
if !ok {
731+
t.Fatalf("Expected manager attribute, got %v", data.Attributes)
732+
}
733+
734+
if manager["firstname"] != "Dave" {
735+
t.Fatalf("Expected manager.firstname to be \"Dave\", got %v", manager)
736+
}
737+
}
738+
685739
func TestOmitsZeroTimes(t *testing.T) {
686740
testModel := &Blog{
687741
ID: 5,

0 commit comments

Comments
 (0)