Search code examples
gogenericsreflectionmappingtype-parameter

How to extract type parameters using reflection


Context: I'm writing a generic auto-mapper that takes two types of structs, checks each field of said structs for a given tag, then copies the value from the source struct to the target struct assuming that they have matching tags and types. Whenever a struct field is another (nested) struct, I want the auto-mapper function to do a recursive call, auto-mapping all the way down the rabbit hole.

Problem: I'm only able to pass the concrete type of the root structs. Once I'm inside the generic function that's using reflection, trying to extract the nested struct types doesn't seem possible. While I can pass the Value.Interface() as an argument, I still need to pass the type parameters as well.

Here is some simplified code to show the problem.

type Alpha struct {
    Nested Beta `automap:"nested"`
}

type Beta struct {
    Info string `automap:"info"`
}

type Foo struct {
    Nested Bar `automap:"nested"`
}

type Bar struct {
    Info string `automap:"info"`
}

func TestAutoMap(t *testing.T) {

    b := Beta{Info: "Hello from Beta!"}
    a := Alpha{Nested: b}

    f, err := AutoMap[Alpha, Foo](a)
    if err != nil {
        fmt.Println(err)
        t.Fail()
    }
    fmt.Println("f.nested.info:", f.Nested.Info)
}

func AutoMap[S, T any](source S) (target T, err error) {

    targetStruct := reflect.ValueOf(&target).Elem()
    sourceStruct := reflect.ValueOf(&source).Elem()

    // .Type and .Kind directly did not work.
    nestedSourceType := ??? // I want this to be type Beta.
    nestedTargetType := ??? // I want this to be type Bar.

    sourceInterface := sourceStruct.Interface()

    t, err := AutoMap[nestedSourceType, nestedTargetType](sourceInterface)
    if err != nil {
        return target, err
    }
    target = t

    return target, nil
}

Solution

  • Having followed the advice of @mkopriva, I want to share a simple solution to the problem I had.

    Please feel free to correct or improve it, but keep in mind that I am deliberately not including all sorts of checks and assertions below.

    (Go Playground Example)

    type Alpha struct {
        NestedOnce Beta
    }
    
    type Beta struct {
        NestedTwice Gamma
    }
    
    type Gamma struct {
        Info string
    }
    
    type Foo struct {
        NestedOnce Bar
    }
    
    type Bar struct {
        NestedTwice Baz
    }
    
    type Baz struct {
        Info string
    }
    
    func TestAutoMap(t *testing.T) {
    
        g := Gamma{"Hello from Gamma!"}
        b := Beta{g}
        a := Alpha{b}
    
        f, err := AutoMap[Foo](a)
        if err != nil {
            fmt.Println(err)
            t.Fail()
        } else {
            fmt.Println("Foo.NestedOnce.NestedTwice.Info:", f.NestedOnce.NestedTwice.Info)
        }
    }
    
    func AutoMap[T any](source any) (target T, err error) {
    
        // Peel off 'any' from the function parameter type.
        sourceStruct := reflect.ValueOf(&source).Elem().Elem()
    
        targetStruct := reflect.ValueOf(&target).Elem()
    
        err = autoMap(sourceStruct, targetStruct)
    
        return target, err
    }
    
    func autoMap(s, t reflect.Value) error {
    
        sourceField := s.Field(0)
        targetField := t.Field(0)
    
        if sourceField.Kind() == reflect.Struct {
            err := autoMap(sourceField, targetField)
            if err != nil {
                return err
            }
            return nil
        }
    
        targetField.Set(sourceField)
    
        return nil
    }