Skip to content

Commit fae13ce

Browse files
authored
Merge pull request #30 from hashicorp/brandonc/handle_json_annotation_marshaling
Support nested object attributes with `json` annotations
2 parents 0f6f733 + e463f7b commit fae13ce

File tree

1 file changed

+57
-20
lines changed

1 file changed

+57
-20
lines changed

Diff for: response.go

+57-20
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,18 @@ func selectChoiceTypeStructField(structValue reflect.Value) (reflect.Value, erro
221221
return reflect.Value{}, errors.New("no non-nil choice field was found in the specified struct")
222222
}
223223

224+
// hasJSONAPIAnnotations returns true if any of the fields of a struct type t
225+
// has a jsonapi annotation. This function will panic if t is not a struct type.
226+
func hasJSONAPIAnnotations(t reflect.Type) bool {
227+
for i := 0; i < t.NumField(); i++ {
228+
tag := t.Field(i).Tag.Get(annotationJSONAPI)
229+
if tag != "" {
230+
return true
231+
}
232+
}
233+
return false
234+
}
235+
224236
func visitModelNodeAttribute(args []string, node *Node, fieldValue reflect.Value) error {
225237
var omitEmpty, iso8601, rfc3339 bool
226238

@@ -314,31 +326,56 @@ func visitModelNodeAttribute(args []string, node *Node, fieldValue reflect.Value
314326
if fieldValue.Len() == 0 && omitEmpty {
315327
return nil
316328
}
317-
// Nested slice of object attributes
318-
manyNested, err := visitModelNodeRelationships(fieldValue, nil, false)
319-
if err != nil {
320-
return fmt.Errorf("failed to marshal slice of nested attribute %q: %w", args[1], err)
329+
330+
var t reflect.Type
331+
if isSliceOfStruct {
332+
t = fieldValue.Type().Elem()
333+
} else {
334+
t = fieldValue.Type().Elem().Elem()
321335
}
322-
nestedNodes := make([]any, len(manyNested.Data))
323-
for i, n := range manyNested.Data {
324-
nestedNodes[i] = n.Attributes
336+
337+
// This check is to maintain backwards compatibility with `json` annotated
338+
// nested structs, which should fall through to "primitive" handling below
339+
if hasJSONAPIAnnotations(t) {
340+
// Nested slice of object attributes
341+
manyNested, err := visitModelNodeRelationships(fieldValue, nil, false)
342+
if err != nil {
343+
return fmt.Errorf("failed to marshal slice of nested attribute %q: %w", args[1], err)
344+
}
345+
nestedNodes := make([]any, len(manyNested.Data))
346+
for i, n := range manyNested.Data {
347+
nestedNodes[i] = n.Attributes
348+
}
349+
node.Attributes[args[1]] = nestedNodes
350+
return nil
325351
}
326-
node.Attributes[args[1]] = nestedNodes
327352
} else if isStruct || isPointerToStruct {
328-
// Nested object attribute
329-
nested, err := visitModelNode(fieldValue.Interface(), nil, false)
330-
if err != nil {
331-
return fmt.Errorf("failed to marshal nested attribute %q: %w", args[1], err)
332-
}
333-
node.Attributes[args[1]] = nested.Attributes
334-
} else {
335-
// Primitive attribute
336-
strAttr, ok := fieldValue.Interface().(string)
337-
if ok {
338-
node.Attributes[args[1]] = strAttr
353+
var t reflect.Type
354+
if isStruct {
355+
t = fieldValue.Type()
339356
} else {
340-
node.Attributes[args[1]] = fieldValue.Interface()
357+
t = fieldValue.Type().Elem()
341358
}
359+
360+
// This check is to maintain backwards compatibility with `json` annotated
361+
// nested structs, which should fall through to "primitive" handling below
362+
if hasJSONAPIAnnotations(t) {
363+
// Nested object attribute
364+
nested, err := visitModelNode(fieldValue.Interface(), nil, false)
365+
if err != nil {
366+
return fmt.Errorf("failed to marshal nested attribute %q: %w", args[1], err)
367+
}
368+
node.Attributes[args[1]] = nested.Attributes
369+
return nil
370+
}
371+
}
372+
373+
// Primitive attribute
374+
strAttr, ok := fieldValue.Interface().(string)
375+
if ok {
376+
node.Attributes[args[1]] = strAttr
377+
} else {
378+
node.Attributes[args[1]] = fieldValue.Interface()
342379
}
343380
}
344381

0 commit comments

Comments
 (0)