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)
}