😀
Golangでタグに応じた構造体→Mapの変換
背景
弊社では現在Firestoreを用いて開発をしているのですが、Firebase admin SDKを利用してデータベースとやり取りする際、Createは構造体を用いてタグの内容に沿って実行されますが、Set時、firebase.MergeAllを利用する時はMapへの変換を求められ、Map変換すると色んな不具合が起こったため、タグの内容に沿ったMapを作成できる関数を作りたいと思いました。
結論
下記になります。
package main
import (
"fmt"
"reflect"
"strings"
"time"
)
func StructToMap(val interface{}) (map[string]interface{}, error) {
return structToMapRecursive(val)
}
func structToMapRecursive(val interface{}) (map[string]interface{}, error) {
value := reflect.ValueOf(val)
typ := reflect.TypeOf(val)
if value.Kind() != reflect.Struct {
return nil, fmt.Errorf("only structs are supported")
}
mapVal := make(map[string]interface{})
for i := 0; i < value.NumField(); i++ {
field := value.Field(i)
fieldType := typ.Field(i)
firestoreTag := fieldType.Tag.Get("firestore")
if firestoreTag == "-" {
continue
}
jsonTag := fieldType.Tag.Get("json")
if jsonTag != "" {
jsonTag = strings.Split(jsonTag, ",")[0]
} else {
jsonTag = fieldType.Name
}
switch field.Kind() {
case reflect.String:
if strVal := field.String(); strVal != "" {
mapVal[jsonTag] = strVal
}
case reflect.Int, reflect.Int64, reflect.Int32:
if intVal := field.Int(); intVal != 0 {
mapVal[jsonTag] = intVal
}
case reflect.Struct:
if field.Type() == reflect.TypeOf(time.Time{}) {
if timeVal := field.Interface().(time.Time); !timeVal.IsZero() {
mapVal[jsonTag] = timeVal
}
} else {
nestedMap, err := structToMapRecursive(field.Interface())
if err != nil {
return nil, err
}
if len(nestedMap) > 0 {
mapVal[jsonTag] = nestedMap
}
}
case reflect.Slice, reflect.Array:
len := field.Len()
slice := make([]interface{}, len)
for i := 0; i < len; i++ {
elem := field.Index(i)
if elem.Kind() == reflect.Struct {
mappedElem, err := structToMapRecursive(elem.Interface())
if err != nil {
return nil, err
}
slice[i] = mappedElem
} else {
slice[i] = elem.Interface()
}
}
mapVal[jsonTag] = slice
case reflect.Bool:
boolVal := field.Bool()
mapVal[jsonTag] = boolVal
case reflect.Float32, reflect.Float64:
floatVal := field.Float()
if floatVal != 0.0 {
mapVal[jsonTag] = floatVal
}
case reflect.Ptr:
if !field.IsNil() {
elem := field.Elem()
nestedMap, err := structToMapRecursive(elem.Interface())
if err != nil {
return nil, err
}
if len(nestedMap) > 0 {
mapVal[jsonTag] = nestedMap
}
}
}
}
return mapVal, nil
}
お試し用コード
package main
import (
"fmt"
"reflect"
"strings"
"time"
)
type Address struct {
City string `json:"city" firestore:"-"`
State string `json:"state" firestore:"state"`
}
type Person struct {
Name string `json:"name" firestore:"name"`
Age int `json:"age" firestore:"age"`
Date time.Time `json:"date" firestore:"date"`
Address Address `json:"address" firestore:"-"`
Hide string `json:"hide" firestore:"-"`
}
func StructToMap(val interface{}) (map[string]interface{}, error) {
return structToMapRecursive(val)
}
func structToMapRecursive(val interface{}) (map[string]interface{}, error) {
value := reflect.ValueOf(val)
typ := reflect.TypeOf(val)
if value.Kind() != reflect.Struct {
return nil, fmt.Errorf("only structs are supported")
}
mapVal := make(map[string]interface{})
for i := 0; i < value.NumField(); i++ {
field := value.Field(i)
fieldType := typ.Field(i)
firestoreTag := fieldType.Tag.Get("firestore")
if firestoreTag == "-" {
continue
}
jsonTag := fieldType.Tag.Get("json")
if jsonTag != "" {
jsonTag = strings.Split(jsonTag, ",")[0]
} else {
jsonTag = fieldType.Name
}
switch field.Kind() {
case reflect.String:
if strVal := field.String(); strVal != "" {
mapVal[jsonTag] = strVal
}
case reflect.Int, reflect.Int64, reflect.Int32:
if intVal := field.Int(); intVal != 0 {
mapVal[jsonTag] = intVal
}
case reflect.Struct:
if field.Type() == reflect.TypeOf(time.Time{}) {
if timeVal := field.Interface().(time.Time); !timeVal.IsZero() {
mapVal[jsonTag] = timeVal
}
} else {
nestedMap, err := structToMapRecursive(field.Interface())
if err != nil {
return nil, err
}
if len(nestedMap) > 0 {
mapVal[jsonTag] = nestedMap
}
}
}
}
return mapVal, nil
}
func main() {
addr := Address{City: "NY", State: "NY"}
p := Person{Name: "", Age: 30, Date: time.Now(), Address: addr, Hide: "hidden"}
result, err := StructToMap(p)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(result)
}
これのおかげで、不要なフィールドがドキュメントに作られることなく、変更したい値だけをセットした構造体でFirestoreの上書きができるようになりました。(まだ検証甘いためバグはあるかもです。。。)
余談
今更の話かもですが、ChatGPTで作成したコードをオンライン実行環境で一旦使ってみること大事だなと思いました。個人的に、GoはGo Playgroundがおすすめ。
間違いやもっと良い方法あったら是非教えて下さい。
追記
- 配列、スライス、float、bool、ポインタに対応できていなかったのでその部分を追加しました。
Discussion
私も似たようなコードを書いたことがあって、同様に似たような見落としをしたことがあるのですがembedされたstructのハンドリングがされてないですね。
この辺をどうするかは
encoding/json
の中身を読むと大変複雑なことがわかります。githubを適当に検索するといろいろ出てきますが、go-modelがぱっと見うまく動きそうです
map[string]any
からstructureへの値のコピーはmapstructureが人気なのですが、試してみるとstructからmap[string]any
への変換時にtime.Timeが空のmap(map[]
)になるなどうまく取り扱えないようです(issue)。参考になれば幸いです。
@ngicks
こちら確認が遅くなってしまいすみません。
ご指摘ありがとうございます!
大変勉強になりました。
確かにembedについて完全に考慮漏れしていました。
go-model、非常に便利そうですね!