Possible to get JSON tag of a single field within a struct in golang? Not the entire struct's fields

huangapple go评论89阅读模式
英文:

Possible to get JSON tag of a single field within a struct in golang? Not the entire struct's fields

问题

与StackOverflow上的这个问题类似,但我想要能够获取结构体中单个字段的JSON标签,而不是结构体的所有标签。

我正在尝试做的事情是:
我正在为服务器编写一个编辑API,但只会发送正在编辑的值。我为我的Postgres服务器编写了单独的更新函数,所以我希望能够做到这样的事情。

伪代码:

type Example struct {
    title String (json字段)
    publisher String (json字段)
}

var json ...

if fieldExists(title) {
    updateTitle(json[getField(example.title))
}
英文:

Similar to this question on StackOverflow, except I want to be able to get the JSON tag of a single field within a struct, not all the tags for a struct.

Get JSON field names of a struct

What I'm trying to do:
I'm writing an edit API for a server, but only the values that are being edited will be sent in. I have individual update function for my Postgres server so I'd like to be able to do something like this.

pseudocode:

type Example struct {
    title String (json field)
    publisher String (json field)
 }

 var json ...

if fieldExists(title) {
     updateTitle(json[getField(example.title))
}

答案1

得分: 2

使用结构体值和字段名称来获取标签:

// jsonTag函数返回结构体值v中具有给定字段名称的json字段标签。
// 如果结构体没有具有给定名称的字段或命名字段没有JSON标签,则该函数返回""和false。
func jsonTag(v interface{}, fieldName string) (string, bool) {
t := reflect.TypeOf(v)
sf, ok := t.FieldByName(fieldName)
if !ok {
return "", false
}
return sf.Tag.Lookup("json")
}

示例:

fmt.Println(jsonTag(Example{}, "Foo"))

https://go.dev/play/p/-47m7ZQ_-24

你无法从字段中获取标签,因为标签是结构体类型的一部分,而不是字段类型的一部分。

英文:

Use a struct value and the name of the field to get the tag:

// jsonTag returns the json field tag with given field name 
// in struct value v.  The function returns "", false if the
// struct does not have a field with the given name or the 
// the named field does not have a JSON tag.
func jsonTag(v interface{}, fieldName string) (string, bool) {
	t := reflect.TypeOf(v)
	sf, ok := t.FieldByName(fieldName)
	if !ok {
		return "", false
	}
	return sf.Tag.Lookup("json")
}

Example:

fmt.Println(jsonTag(Example{}, "Foo"))

https://go.dev/play/p/-47m7ZQ_-24

You cannot get the tag from the field because the tag is part of the struct's type, not part of the field's type.

答案2

得分: 1

更新于2023/02/27

我的原始答案中的所有辅助代码现在都封装在github.com/jellydator/validation模块中。现在,解决方案只需要调用validation.ErrorFieldName函数。

type UpdateRequest struct {
	Description     string `json:"description,omitempty"`
	PrivatePods     bool   `json:"private_pods"`
	OperatingMode   string `json:"operating_mode,omitempty"`
	ActivationState string `json:"activation_state,omitempty"`
}

func main() {
	var request UpdateRequest

	jsonTag, err2 := validation.ErrorFieldName(&request, &request.OperatingMode)
	if err2 != nil {
		fmt.Println("在结构体中找不到字段")
		return
	}

	fmt.Println("JSON字段名:" + jsonTag)
}

在Go Playground上查看更新后的解决方案的运行效果:
https://go.dev/play/p/AcS6vvrfOY4

原始答案

可以通过仅使用结构体和字段指针来获取结构体中单个字段的JSON标签。需要明确的是,此解决方案不需要事先知道字段的字符串形式名称。这很重要,因为这样的解决方案更有可能在将来进行重构时保持有效,而重构可能会更改字段的名称。

我的解决方案大部分代码都是从https://github.com/jellydator/validation的请求验证库中借用的。

代码的工作原理如下:

type UpdateRequest struct {
	Description     string `json:"description,omitempty"`
	PrivatePods     bool   `json:"private_pods"`
	OperatingMode   string `json:"operating_mode,omitempty"`
	ActivationState string `json:"activation_state,omitempty"`
}

func main() {
	var request UpdateRequest

	jsonTag, err2 := FindStructFieldJSONName(&request, &request.OperatingMode)
	if err2 != nil {
		fmt.Println("在结构体中找不到字段")
		return
	}

	fmt.Println("JSON字段名:" + jsonTag)
}

上述示例的输出为:

JSON字段名:operating_mode

如您所见,只需要一个指向结构体的指针和一个指向该结构体中字段的指针作为输入。代码的工作原理如下...

FindStructFieldJSONName - 此函数对结构体指针进行一些反射操作,以获取一些值并断言指针确实指向结构体。它还对字段指针进行一些反射操作,以断言它确实指向字段。然后,该函数调用findStructField来查找结构体中的字段,从而确保字段指针指向结构体中的字段。最后,该函数调用getErrorFieldName来获取JSON标签的值。

findStructField - 使用反射获取给定结构体中给定字段的*reflect.StructField

getErrorFieldName - 使用反射从字段中读取JSON标签。

注意:在OP的问题和本答案的上下文中,函数名getErrorFieldName并不太合适。更好的命名可能是getJSONFieldName。我从使用此代码的库中复制了此函数,但选择不对其进行重命名,以便您可以更轻松地参考原始源代码。

您可以在Go Playground上尝试它。

https://go.dev/play/p/7BWjPWX1G7d

以下是供参考的完整代码:

// You can edit this code!
// Click here and start typing.
package main

import (
	"fmt"
	"reflect"
	"strings"

	"github.com/friendsofgo/errors"
)

const (
	// ErrorTag是用于自定义结构体字段的错误字段名称的结构体标签名称。
	ErrorTag = "json"
)

var (
	ErrInternal = errors.New("内部验证错误")
)

// FindStructFieldJSONName获取给定结构体中给定字段的`json`标签的值
// 实现灵感来自https://github.com/jellydator/validation/blob/v1.0.0/struct.go#L70
func FindStructFieldJSONName(structPtr interface{}, fieldPtr interface{}) (string, error) {
	value := reflect.ValueOf(structPtr)
	if value.Kind() != reflect.Ptr || !value.IsNil() && value.Elem().Kind() != reflect.Struct {
		// 必须是指向结构体的指针
		return "", errors.Wrap(ErrInternal, "必须是指向结构体的指针")
	}
	if value.IsNil() {
		// 将nil结构体指针视为有效
		return "", nil
	}
	value = value.Elem()

	fv := reflect.ValueOf(fieldPtr)
	if fv.Kind() != reflect.Ptr {
		return "", errors.Wrap(ErrInternal, "必须是指向字段的指针")
	}
	ft := findStructField(value, fv)
	if ft == nil {
		return "", errors.Wrap(ErrInternal, "找不到字段")
	}
	return getErrorFieldName(ft), nil
}

// findStructField在给定结构体中查找字段。
// 要查找的字段应该是指向实际结构体字段的指针。
// 如果找到,将返回字段信息。否则,将返回nil。
// 实现借用自https://github.com/jellydator/validation/blob/v1.0.0/struct.go#L134
func findStructField(structValue reflect.Value, fieldValue reflect.Value) *reflect.StructField {
	ptr := fieldValue.Pointer()
	for i := structValue.NumField() - 1; i >= 0; i-- {
		sf := structValue.Type().Field(i)
		if ptr == structValue.Field(i).UnsafeAddr() {
			// 进行额外的类型比较,因为嵌入结构的地址可能与嵌入结构的第一个字段的地址相同
			if sf.Type == fieldValue.Elem().Type() {
				return &sf
			}
		}
		if sf.Anonymous {
			// 深入匿名结构以查找字段
			fi := structValue.Field(i)
			if sf.Type.Kind() == reflect.Ptr {
				fi = fi.Elem()
			}
			if fi.Kind() == reflect.Struct {
				if f := findStructField(fi, fieldValue); f != nil {
					return f
				}
			}
		}
	}
	return nil
}

// getErrorFieldName返回应用于表示结构体字段的验证错误的名称。
// 实现借用自https://github.com/jellydator/validation/blob/v1.0.0/struct.go#L162
func getErrorFieldName(f *reflect.StructField) string {
	if tag := f.Tag.Get(ErrorTag); tag != "" && tag != "-" {
		if cps := strings.SplitN(tag, ",", 2); cps[0] != "" {
			return cps[0]
		}
	}
	return f.Name
}

type UpdateRequest struct {
	Description     string `json:"description,omitempty"`
	PrivatePods     bool   `json:"private_pods"`
	OperatingMode   string `json:"operating_mode,omitempty"`
	ActivationState string `json:"activation_state,omitempty"`
}

func main() {
	var request UpdateRequest

	jsonTag, err2 := FindStructFieldJSONName(&request, &request.OperatingMode)
	if err2 != nil {
		fmt.Println("在结构体中找不到字段")
		return
	}

	fmt.Println("JSON字段名:" + jsonTag)
}
英文:

UPDATE 2023/02/27

All of the helper code in my original answer is now encapsulated in the github.com/jellydator/validation module. The solution is now just a single function call to validation.ErrorFieldName.

type UpdateRequest struct {
Description     string `json:"description,omitempty"`
PrivatePods     bool   `json:"private_pods"`
OperatingMode   string `json:"operating_mode,omitempty"`
ActivationState string `json:"activation_state,omitempty"`
}
func main() {
var request UpdateRequest
jsonTag, err2 := validation.ErrorFieldName(&request, &request.OperatingMode)
if err2 != nil {
fmt.Println("field not found in struct")
return
}
fmt.Println("JSON field name: " + jsonTag)
}

See updated solution in action at the Go Playground
https://go.dev/play/p/AcS6vvrfOY4

ORIGINAL ANSWER

It is possible to get the JSON tag of a single field within a struct using only the struct and a pointer to the field. To be clear, this solution doesn't require any prior knowledge of the name of the field in string form. This is important because this solution is more likely to survive future refactoring that might change the name of the field.

Most of my solution is code borrowed from the request validation library at https://github.com/jellydator/validation

The code works like this

type UpdateRequest struct {
Description     string `json:"description,omitempty"`
PrivatePods     bool   `json:"private_pods"`
OperatingMode   string `json:"operating_mode,omitempty"`
ActivationState string `json:"activation_state,omitempty"`
}
func main() {
var request UpdateRequest
jsonTag, err2 := FindStructFieldJSONName(&request, &request.OperatingMode)
if err2 != nil {
fmt.Println("field not found in struct")
return
}
fmt.Println("JSON field name: " + jsonTag)
}

The output from the above example is

JSON field name: operating_mode

As you can see the only two inputs are a pointer to a struct and a pointer to a field within that struct. The code works like this...

FindStructFieldJSONName - This function does a little reflection on the struct pointer to get some values and assert that the pointer really points to a struct. It also does some reflection on the field pointer to assert it really does point to a field. This function then calls findStructField to find the field within the struct, essentially making sure the field pointer points to a field within the struct. Finally, this function calls getErrorFieldName to get the value of the json tag.

findStructField - uses reflection to get a *reflect.StructField for the given field within the given struct

getErrorFieldName - uses reflection to read the json tag from the field.

NOTE: In the context of OP's question and the context of this answer the function name, getErrorFieldName, doesn't make much sense. It might be better named as getJSONFieldName. I copied this function from library that uses this code in a different context. I chose not to rename any of it so that you can more easily refer to the original source.

You can try it out at the Go Playground.

https://go.dev/play/p/7BWjPWX1G7d

Complete code posted here for reference:

// You can edit this code!
// Click here and start typing.
package main
import (
"fmt"
"reflect"
"strings"
"github.com/friendsofgo/errors"
)
const (
// ErrorTag is the struct tag name used to customize the error field name for a struct field.
ErrorTag = "json"
)
var (
ErrInternal = errors.New("internal validation error")
)
// FindStructFieldJSONName gets the value of the `json` tag for the given field in the given struct
// Implementation inspired by https://github.com/jellydator/validation/blob/v1.0.0/struct.go#L70
func FindStructFieldJSONName(structPtr interface{}, fieldPtr interface{}) (string, error) {
value := reflect.ValueOf(structPtr)
if value.Kind() != reflect.Ptr || !value.IsNil() && value.Elem().Kind() != reflect.Struct {
// must be a pointer to a struct
return "", errors.Wrap(ErrInternal, "must be a pointer to a struct")
}
if value.IsNil() {
// treat a nil struct pointer as valid
return "", nil
}
value = value.Elem()
fv := reflect.ValueOf(fieldPtr)
if fv.Kind() != reflect.Ptr {
return "", errors.Wrap(ErrInternal, "must be a pointer to a field")
}
ft := findStructField(value, fv)
if ft == nil {
return "", errors.Wrap(ErrInternal, "field not found")
}
return getErrorFieldName(ft), nil
}
// findStructField looks for a field in the given struct.
// The field being looked for should be a pointer to the actual struct field.
// If found, the field info will be returned. Otherwise, nil will be returned.
// Implementation borrowed from https://github.com/jellydator/validation/blob/v1.0.0/struct.go#L134
func findStructField(structValue reflect.Value, fieldValue reflect.Value) *reflect.StructField {
ptr := fieldValue.Pointer()
for i := structValue.NumField() - 1; i >= 0; i-- {
sf := structValue.Type().Field(i)
if ptr == structValue.Field(i).UnsafeAddr() {
// do additional type comparison because it's possible that the address of
// an embedded struct is the same as the first field of the embedded struct
if sf.Type == fieldValue.Elem().Type() {
return &sf
}
}
if sf.Anonymous {
// delve into anonymous struct to look for the field
fi := structValue.Field(i)
if sf.Type.Kind() == reflect.Ptr {
fi = fi.Elem()
}
if fi.Kind() == reflect.Struct {
if f := findStructField(fi, fieldValue); f != nil {
return f
}
}
}
}
return nil
}
// getErrorFieldName returns the name that should be used to represent the validation error of a struct field.
// Implementation borrowed from https://github.com/jellydator/validation/blob/v1.0.0/struct.go#L162
func getErrorFieldName(f *reflect.StructField) string {
if tag := f.Tag.Get(ErrorTag); tag != "" && tag != "-" {
if cps := strings.SplitN(tag, ",", 2); cps[0] != "" {
return cps[0]
}
}
return f.Name
}
type UpdateRequest struct {
Description     string `json:"description,omitempty"`
PrivatePods     bool   `json:"private_pods"`
OperatingMode   string `json:"operating_mode,omitempty"`
ActivationState string `json:"activation_state,omitempty"`
}
func main() {
var request UpdateRequest
jsonTag, err2 := FindStructFieldJSONName(&request, &request.OperatingMode)
if err2 != nil {
fmt.Println("field not found in struct")
return
}
fmt.Println("JSON field name: " + jsonTag)
}

huangapple
  • 本文由 发表于 2022年1月8日 13:05:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/70629741.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定