3

I am working with an api and I need to pass it a slice of structs. I have a slice of maps so I need to convert it to a slice of structs.

package main

import "fmt"

func main() {
    a := []map[string]interface{}{}
    b := make(map[string]interface{})
    c := make(map[string]interface{})
    
    b["Prop1"] = "Foo"
    b["Prop2"] = "Bar"
    a = append(a, b)

    c["Prop3"] = "Baz"
    c["Prop4"] = "Foobar"
    a = append(a, c)

    fmt.Println(a)
}

[map[Prop1:Foo Prop2:Bar] map[Prop3:Baz Prop4:Foobar]]

so in this example, I have the slice of maps a, which contains b and c which are maps of strings with different keys.

I'm looking to convert a to a slice of structs where the first element is a struct with Prop1 and Prop2 as properties, and where the second element is a struct with Prop3 and Prop4 as properties.

Is this possible?

I've looked at https://github.com/mitchellh/mapstructure but I wasn't able to get it working for my use case. I've looked at this answer: https://stackoverflow.com/a/26746461/3390419

which explains how to use the library:

mapstructure.Decode(myData, &result)

however this seems to assume that the struct of which result is an instance is predefined, whereas in my case the structure is dynamic.

  • 1
    *"Is this possible?"* -- Yes. *"I wasn't able to get it working for my use case"* -- Be more specific, what problems exactly did you encounter. Most importantly however, show the code, show what you've tried. – mkopriva Dec 03 '22 at 20:39
  • Also you should mention whether you want to decode the map into existing types or whether you want to create the structs dynamically (using reflection). – mkopriva Dec 03 '22 at 20:42
  • @mkopriva thank you for your comment. I've clarified that in the question; I'm looking to create the structs dynamically – NeepsAndTatties Dec 03 '22 at 20:50
  • Then you have to first loop over each map individually, using the key-value pairs of a map to construct a corresponding slice of `reflect.StructField` instances. Once you have such a slice ready you can pass it to [`reflect.StructOf`](https://pkg.go.dev/reflect@go1.19.3#StructOf), that will return a `reflect.Type` value that represents the dynamic struct type, you can then pass that to `reflect.New` to create a `reflect.Value` which represents an instance of the dynamic struct (actually pointer to the struct). – mkopriva Dec 03 '22 at 20:57
  • Once you have that you can use the `reflect.Value` together with the corresponding map to assign the struct fields' values. – mkopriva Dec 03 '22 at 20:58
  • @mkopriva thank you for your suggestions. I'm trying it out. It looks like `reflect.StructField` requires a few parameters, what should PkgPath and Offset be? I couldn't find an example in the documentation – NeepsAndTatties Dec 04 '22 at 12:25
  • Providing just the `Name` and `Type` should be enough. You don't need the others for basic struct fields, I think. For example `PkgPath` is needed only for unexported fields (those strating with lowercase) and even then, if you omit it, I'm not certain `StructOf` would complain. Have you seen the example in the documentation under [`StructOf`](https://pkg.go.dev/reflect@go1.19.3#StructOf)? – mkopriva Dec 04 '22 at 12:40
  • https://cs.opensource.google/go/go/+/refs/tags/go1.19.3:src/reflect/example_test.go;l=131-168;drc=81a9a7f4c293794855ed640cdc53835f566b6414 – mkopriva Dec 04 '22 at 12:41
  • And the [docs](https://pkg.go.dev/reflect@go1.19.3#StructOf) actually say this: *"The Offset and Index fields are ignored and computed as they would be by the compiler."* So Offset and Index you can't even set through StructOf. – mkopriva Dec 04 '22 at 12:45
  • @mkopriva thank you, I think I'm getting somewhere with your suggestions. https://go.dev/play/p/k2lRUzFKenz ; now that I have the `reflect.Value`, how do I populate it with the struct field values? – NeepsAndTatties Dec 04 '22 at 12:51
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/250138/discussion-between-mkopriva-and-neepsandtatties). – mkopriva Dec 04 '22 at 13:03

1 Answers1

1

What you can do is to first loop over each map individually, using the key-value pairs of each map you construct a corresponding slice of reflect.StructField values. Once you have such a slice ready you can pass it to reflect.StructOf, that will return a reflect.Type value that represents the dynamic struct type, you can then pass that to reflect.New to create a reflect.Value which will represent an instance of the dynamic struct (actually pointer to the struct).

E.g.

var result []any
for _, m := range a {
    fields := make([]reflect.StructField, 0, len(m))

    for k, v := range m {
        f := reflect.StructField{
            Name: k,
            Type: reflect.TypeOf(v), // allow for other types, not just strings
        }
        fields = append(fields, f)
    }

    st := reflect.StructOf(fields) // new struct type
    sv := reflect.New(st)          // new struct value

    for k, v := range m {
        sv.Elem(). // dereference struct pointer
                FieldByName(k).         // get the relevant field
                Set(reflect.ValueOf(v)) // set the value of the field
    }

    result = append(result, sv.Interface())
}

https://go.dev/play/p/NzHQzKwhwLH

mkopriva
  • 35,176
  • 4
  • 57
  • 71
  • Thank you. Given `result`, how can I access the field of the struct by name? I tried: `fmt.Println(result[0].Prop1)` but this seems to error out with: *./prog.go:45:24: result[0].Prop1 undefined (type any has no field or method Prop1)* – NeepsAndTatties Dec 04 '22 at 14:02
  • Because the type is unknown at compile time you cannot use plain selectors (i.e., `v.field` is illegal). You cannot use dynamically created struct types just like you would statically declared struct types. To access fields of essentially unknown structs your only option is to again use reflection. E.g. `reflect.ValueOf(v).FieldByName("Prop1")`, and if `v` is a pointer to struct add the `.Elem()` call to dereference it. – mkopriva Dec 04 '22 at 14:11
  • so `reflect.ValueOf(result[0])` ? – NeepsAndTatties Dec 04 '22 at 16:43
  • Yes, replace `v` with the value of the unknown struct, e.g. `result[0]`. – mkopriva Dec 04 '22 at 17:06