Golang中(un-)marshalling时处理不同字段名的模式

Golang中(un-)marshalling时处理不同字段名的模式 我正在封装一个不受我控制的REST API,以使其更易于使用。我从该API获取的JSON数据中,大部分字段名称都难以理解,例如:

type Foo struct {
    Bar string `json:"b_Ar"`
    Baz string `json:"no_relation_to_baz_whatsoever"`
}

使用这个结构体,我可以正确地从外部REST API反序列化数据。然而,如果有人试图再次序列化我的结构体,他们将得到我原本试图隐藏的那些难以理解的字段。

据我所知,无法在序列化和反序列化过程中指定不同的字段名称。因此,我目前采用以下方法:

type Foo struct {
    Bar string `json:"bar"`
    Baz string `json:"baz"`
}

func (f *Foo) UnmarshalJSON(data []byte) error {
	type rawFoo struct {
		Bar string `json:"b_Ar"`
		Baz string `json:"no_relation_to_baz_whatsoever"`
	}
	var rf rawFoo
	if err := json.Unmarshal(data, &rf); err != nil {
		return err
	}
	f.Bar = rf.Bar
	f.Baz = rf.Baz
	return nil
}

这种方法可行,但由于存在大量样板代码,编写和维护起来很不方便。我想我可以编写一个辅助函数,例如 func(data []byte, f *Foo, rf rawFoo),利用反射在内部找出匹配的字段。

但我想听听其他人的意见,看看是否有我遗漏的更简单的方法。对于如何改进这一点,有什么想法吗?


更多关于Golang中(un-)marshalling时处理不同字段名的模式的实战教程也可以访问 https://www.itying.com/category-94-b0.html

6 回复

如果我没理解错文档的话,从 Go 1.8 开始,你可以在两个仅标签不同的结构体类型之间直接进行转换:

Go 1.8 Release Notes - The Go Programming Language

所以只需这样写:

foo := foo(rawFoo)

这样做还有一个好处:如果 foorawFoo 中有任何字段不同,你会得到一个编译错误。

更多关于Golang中(un-)marshalling时处理不同字段名的模式的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Dean_Davidson: 但是——我认为,可能就像你目前所做的那样,维护两个结构体或许是最简单的解决方案。虽然冗长,但确实有效。

我同意。你分享的那个使用反射的解决方案(非常感谢你整理出来!)看起来比一些样板代码更难维护。

Dean_Davidson: 我在想,你是否可以直接通过代码生成它们?

也许可以?不过,虽然我目前拥有的类型数量已经多到让我想寻找一个解决方案,但还没有多到让我认为代码生成是一个合适的解决方案 😉

可以通过内联结构体来减少一行代码和一个类型名称。我可能还会为 Foo 添加一条注释:

type Foo struct {
	// 对字段的修改必须与 UnmarshalJSON::expectedJson 同步
	Bar string `json:"bar"`
	Baz string `json:"baz"`
}

func (f *Foo) UnmarshalJSON(data []byte) error {
	var expectedJson struct {
		Bar string `json:"b_Ar"`
		Baz string `json:"no_relation_to_baz_whatsoever"`
	}
	if err := json.Unmarshal(data, &expectedJson); err != nil {
		return err
	}
	*f = Foo(expectedJson)
	return nil
}

成功了! 这意味着我可以将

f.Bar = rf.Bar
f.Baz = rf.Baz

这部分代码替换为一行

*f = Foo(rf)

这样就得到了

type Foo struct {
    Bar string `json:"bar"`
    Baz string `json:"baz"`
}

func (f *Foo) UnmarshalJSON(data []byte) error {
	type rawFoo struct {
		Bar string `json:"b_Ar"`
		Baz string `json:"no_relation_to_baz_whatsoever"`
	}
	var rf rawFoo
	if err := json.Unmarshal(data, &rf); err != nil {
		return err
	}
	*f = Foo(rf)
	return nil
}

这样维护起来并不算太糟。非常感谢!

很遗憾,我一时想不出一个简单/干净的方法来实现这个。我发现了一个问题,有人想做和你完全一样的事情,但没有找到令人满意的解决方案:

github.com/golang/go/issues/64194

提案:encoding/json:允许使用不同的标签

我认为应该可以指定一个不同的标签名称来在序列化 JSON 时使用。

一个用例示例是代理在内部和外部 API 之间重命名字段:

type Example struct {
    CustomerAccountId string `jsonInternal:"kundenKontoId" json:"customerAccountId"`
}

在反序列化时,可以指定 jsonInternal 标签来使用内部名称进行反序列化,而在序列化时(默认)使用普通的 json 标签。

目前的解决方案是拥有两个字段相同但标签不同的结构体,这感觉很混乱。

也许可以创建一个新的结构体标签,然后使用反射创建一个新类型,其中 json 标签是你的新结构体标签。类似这样:

type Example struct {
	Bar  string `json:"bar" incomingjson:"b_Ar"`
	Baz  string `json:"baz" incomingjson:"no_relation_to_baz_whatsoever"`
}

然后,要使用它,你可以这样做:

func CustomUnmarshal[T any](data []byte) (T, error) {
	var returnData T
	objType := reflect.TypeOf(returnData)
	if objType.Kind() == reflect.Ptr {
		objType = objType.Elem()
	}
	if objType.Kind() != reflect.Struct {
		return returnData, fmt.Errorf("expected struct, got %v", objType.Kind())
	}

	// 我们要做的第一件事是创建一个新的结构体类型,用 incomingjson 标签的值替换 json 标签。
	fields := make([]reflect.StructField, objType.NumField())
	for i := 0; i < objType.NumField(); i++ {
		f := objType.Field(i)
		// 如果我们有 incomingjson 标签,就在新结构体类型上设置 json 标签
		incomingjson := f.Tag.Get("incomingjson")
		if len(incomingjson) > 0 {
			f.Tag = reflect.StructTag(fmt.Sprintf(`json:"%v"`, incomingjson))
		}
		fields[i] = f
	}
	// 然后我们进行反序列化
	newEl := reflect.New(reflect.StructOf(fields)).Interface()
	err := json.Unmarshal(data, &newEl)
	if err != nil {
		return returnData, err
	}

	// 接下来,在我们原始的结构体上设置值
	// 目前这是非常不安全的。
	sourceRef := reflect.Indirect(reflect.ValueOf(newEl))
	destRef := reflect.ValueOf(&returnData).Elem()
	for _, field := range fields {
		if destRef.FieldByName(field.Name).CanSet() {
			destRef.FieldByName(field.Name).Set(sourceRef.FieldByName(field.Name))
		}
	}
	return returnData, err
}

这当然不处理带有自定义标签的嵌套结构体,而且可能非常不安全。但这里有一个可运行的示例:

Go Playground - The Go Programming Language

也许这至少可以为你指明一个可能的方向。但是——我认为可能只是像你现在这样维护两个结构体很可能是最简单的解决方案。虽然冗长,但它是有效的。我在想,你是否可以直接用代码生成它们?

在Golang中处理序列化和反序列化时使用不同字段名的需求很常见。你的方法基本正确,但可以通过更简洁的方式实现。以下是几种改进方案:

1. 使用结构体组合和匿名结构体

type Foo struct {
    Bar string `json:"bar"`
    Baz string `json:"baz"`
}

type rawFoo struct {
    Bar string `json:"b_Ar"`
    Baz string `json:"no_relation_to_baz_whatsoever"`
}

func (f *Foo) UnmarshalJSON(data []byte) error {
    var rf rawFoo
    if err := json.Unmarshal(data, &rf); err != nil {
        return err
    }
    *f = Foo(rf)
    return nil
}

2. 使用map作为中间层(适用于简单结构)

func (f *Foo) UnmarshalJSON(data []byte) error {
    var m map[string]interface{}
    if err := json.Unmarshal(data, &m); err != nil {
        return err
    }
    
    if v, ok := m["b_Ar"]; ok {
        f.Bar = v.(string)
    }
    if v, ok := m["no_relation_to_baz_whatsoever"]; ok {
        f.Baz = v.(string)
    }
    return nil
}

3. 使用反射的通用解决方案

func MapFields(data []byte, dst interface{}, fieldMap map[string]string) error {
    var src map[string]interface{}
    if err := json.Unmarshal(data, &src); err != nil {
        return err
    }
    
    dstVal := reflect.ValueOf(dst).Elem()
    dstType := dstVal.Type()
    
    for i := 0; i < dstVal.NumField(); i++ {
        field := dstType.Field(i)
        jsonTag := field.Tag.Get("json")
        if jsonTag == "" || jsonTag == "-" {
            continue
        }
        
        // 获取映射的源字段名
        srcFieldName := fieldMap[jsonTag]
        if srcFieldName == "" {
            srcFieldName = jsonTag
        }
        
        if srcValue, ok := src[srcFieldName]; ok {
            fieldVal := dstVal.Field(i)
            if fieldVal.CanSet() {
                // 类型转换逻辑(简化版,实际需要更完整的类型检查)
                switch fieldVal.Kind() {
                case reflect.String:
                    fieldVal.SetString(srcValue.(string))
                // 添加其他类型的处理...
                }
            }
        }
    }
    return nil
}

// 使用示例
type Foo struct {
    Bar string `json:"bar"`
    Baz string `json:"baz"`
}

func (f *Foo) UnmarshalJSON(data []byte) error {
    fieldMap := map[string]string{
        "bar": "b_Ar",
        "baz": "no_relation_to_baz_whatsoever",
    }
    return MapFields(data, f, fieldMap)
}

4. 使用第三方库

如果结构体复杂,可以考虑使用第三方库如 mapstructure

import "github.com/mitchellh/mapstructure"

type Foo struct {
    Bar string `json:"bar"`
    Baz string `json:"baz"`
}

func (f *Foo) UnmarshalJSON(data []byte) error {
    var raw map[string]interface{}
    if err := json.Unmarshal(data, &raw); err != nil {
        return err
    }
    
    decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
        Result: f,
        TagName: "json",
        DecodeHook: mapstructure.ComposeDecodeHookFunc(
            func(from, to reflect.Type, data interface{}) (interface{}, error) {
                // 字段名映射
                if from.Kind() == reflect.Map {
                    m := data.(map[string]interface{})
                    // 重命名字段
                    if v, ok := m["b_Ar"]; ok {
                        m["bar"] = v
                        delete(m, "b_Ar")
                    }
                    if v, ok := m["no_relation_to_baz_whatsoever"]; ok {
                        m["baz"] = v
                        delete(m, "no_relation_to_baz_whatsoever")
                    }
                }
                return data, nil
            },
        ),
    })
    
    if err != nil {
        return err
    }
    
    return decoder.Decode(raw)
}

5. 使用代码生成工具

对于大量结构体,可以考虑使用代码生成:

//go:generate go run generate.go

// generate.go
package main

import (
    "fmt"
    "go/ast"
    "go/parser"
    "go/token"
    "strings"
    "text/template"
)

// 解析结构体并生成UnmarshalJSON方法

你的当前方案对于简单场景已经足够好。如果字段数量不多,第一种方案最简洁;如果需要处理大量结构体,第三种反射方案或使用第三方库会更合适。

回到顶部