使用mapstruct.DecodeHookFunc将结构编码为映射[字符串]任何内容

问题描述 投票:0回答:2

如何使用 github.com/mitchellh/mapstruct 编码结构,例如:

type Struct struct {
    CreateDate *timestamppb.Timestamp `mapstructure:"create_date"`
}

至:

map[string]any{
    "create_date": "2024-01-01 12:00:00",
}

我正在尝试类似的事情

func TimestamppbToDatetimeHookFunc() mapstructure.DecodeHookFunc {
    return func(f reflect.Type, t reflect.Type, data any) (any, error) {
        if f != reflect.TypeOf(&timestamppb.Timestamp{}) {
            return data, nil
        }

        return data.(*timestamppb.Timestamp).AsTime().Format(time.DateTime), nil
    }
}

还有

m := &Struct{CreateDate: timestamppb.Now()}

data := make(map[string]any)

d, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
    Result:     &data,
    DecodeHook: TimestamppbToDatetimeHookFunc(),
})


if err := d.Decode(m); err != nil {
    return nil, err
}

但是d.解码返回错误

'create_date' expected a map, got 'string'
.

也许我可以用

DecodeHookFuncValue (func(from reflect.Value, to reflect.Value) (interface{}, error))
和 Reflect 覆盖输出预期类型,但是如何实现?

go reflection decode encode
2个回答
1
投票

DecodeHook
应用于整个输入,而不仅仅是
CreateDate
字段,因此f类型是
Struct
,而不是时间戳。这应该有效:

func TimestamppbToDatetimeHookFunc() mapstructure.DecodeHookFunc {
    return func(f reflect.Type, t reflect.Type, data any) (any, error) {
        if f == reflect.TypeOf(&Struct{}) {
            v := data.(*Struct)

            return map[string]interface{}{
                "created_date": v.CreateDate.AsTime().Format(time.DateTime),
            }, nil
        }

        return data, nil
    }
}

0
投票

我感觉

go-viper/mapstructure
并没有很重视这个问题,提出的很多
issues
都被忽略了。我选择用我自己的(仅供参考):

package mapstruct

import (
    "fmt"
    "reflect"
)

func defaultDecoderConfig(config ...*DecoderConfig) *DecoderConfig {
    cfg := append(config, &DecoderConfig{})[0]
    if cfg.TagName == "" {
        cfg.TagName = "json"
    }
    return cfg
}

type DecoderConfig struct {
    DecodeHook func(reflect.Type, any) (any, error)
    TagName    string
}

func Decode(obj any, config ...*DecoderConfig) (map[string]any, error) {
    cfg := defaultDecoderConfig(config...)
    result := make(map[string]any)

    sv := reflect.ValueOf(obj)
    if sv.Kind() == reflect.Ptr {
        sv = sv.Elem() // 解引用指针
    }

    if sv.Kind() != reflect.Struct {
        return nil, fmt.Errorf("expected a struct but got %s", sv.Kind())
    }

    for i, st := 0, sv.Type(); i < sv.NumField(); i++ {
        key := st.Field(i).Tag.Get(cfg.TagName)
        if key == "" || key == "-" {
            continue
        }

        fv := sv.Field(i) // 获得字段的值
        if hook := cfg.DecodeHook; hook != nil {
            value, err := hook(fv.Type(), fv.Interface())
            if err != nil {
                return nil, err
            }
            result[key] = value
            continue
        }

        switch fv.Kind() {
        case reflect.Struct: // 递归处理嵌套结构体
            m, err := Decode(fv.Interface())
            if err != nil {
                return nil, err
            }
            result[key] = m
        default:
            result[key] = fv.Interface()
        }
    }

    return result, nil
}

此实现可以将

struct
转换为
map
,如下所示:

type Person struct {
    Time time.Time `json:"time"`
    Name string    `json:"name"`
}

input := Person{time.Now(), "testName"}

m, err := mapstruct.Decode(input, &mapstruct.DecoderConfig{
    DecodeHook: func(t reflect.Type, a any) (v any, err error) {
        if t == reflect.TypeOf(time.Time{}) {
            value, _ := a.(time.Time)
            return value.Format(time.DateTime), nil
        }
        return a, nil
    },
})

if err != nil {
    panic(err)
}

fmt.Printf("%#v\n", m)
// input => {"name":"testName", "time":"2024-11-22 12:35:55"}
© www.soinside.com 2019 - 2024. All rights reserved.