1

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))
}
StainlessSteelRat
  • 364
  • 1
  • 2
  • 16

2 Answers2

2

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.

1

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)
}
HairOfTheDog
  • 2,489
  • 2
  • 29
  • 35