diff --git a/go.mod b/go.mod index a03ae973..3b40e066 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/mitchellh/mapstructure -go 1.14 +go 1.20 + +require github.com/iancoleman/strcase v0.3.0 diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..6261b6ac --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= diff --git a/mapstructure.go b/mapstructure.go index 7581806a..480c5219 100644 --- a/mapstructure.go +++ b/mapstructure.go @@ -9,84 +9,84 @@ // // The simplest function to start with is Decode. // -// Field Tags +// # Field Tags // // When decoding to a struct, mapstructure will use the field name by // default to perform the mapping. For example, if a struct has a field // "Username" then mapstructure will look for a key in the source value // of "username" (case insensitive). // -// type User struct { -// Username string -// } +// type User struct { +// Username string +// } // // You can change the behavior of mapstructure by using struct tags. // The default struct tag that mapstructure looks for is "mapstructure" // but you can customize it using DecoderConfig. // -// Renaming Fields +// # Renaming Fields // // To rename the key that mapstructure looks for, use the "mapstructure" // tag and set a value directly. For example, to change the "username" example // above to "user": // -// type User struct { -// Username string `mapstructure:"user"` -// } +// type User struct { +// Username string `mapstructure:"user"` +// } // -// Embedded Structs and Squashing +// # Embedded Structs and Squashing // // Embedded structs are treated as if they're another field with that name. // By default, the two structs below are equivalent when decoding with // mapstructure: // -// type Person struct { -// Name string -// } +// type Person struct { +// Name string +// } // -// type Friend struct { -// Person -// } +// type Friend struct { +// Person +// } // -// type Friend struct { -// Person Person -// } +// type Friend struct { +// Person Person +// } // // This would require an input that looks like below: // -// map[string]interface{}{ -// "person": map[string]interface{}{"name": "alice"}, -// } +// map[string]interface{}{ +// "person": map[string]interface{}{"name": "alice"}, +// } // // If your "person" value is NOT nested, then you can append ",squash" to // your tag value and mapstructure will treat it as if the embedded struct // were part of the struct directly. Example: // -// type Friend struct { -// Person `mapstructure:",squash"` -// } +// type Friend struct { +// Person `mapstructure:",squash"` +// } // // Now the following input would be accepted: // -// map[string]interface{}{ -// "name": "alice", -// } +// map[string]interface{}{ +// "name": "alice", +// } // // When decoding from a struct to a map, the squash tag squashes the struct // fields into a single map. Using the example structs from above: // -// Friend{Person: Person{Name: "alice"}} +// Friend{Person: Person{Name: "alice"}} // // Will be decoded into a map: // -// map[string]interface{}{ -// "name": "alice", -// } +// map[string]interface{}{ +// "name": "alice", +// } // // DecoderConfig has a field that changes the behavior of mapstructure // to always squash embedded structs. // -// Remainder Values +// # Remainder Values // // If there are any unmapped keys in the source value, mapstructure by // default will silently ignore them. You can error by setting ErrorUnused @@ -98,20 +98,20 @@ // probably be a "map[string]interface{}" or "map[interface{}]interface{}". // See example below: // -// type Friend struct { -// Name string -// Other map[string]interface{} `mapstructure:",remain"` -// } +// type Friend struct { +// Name string +// Other map[string]interface{} `mapstructure:",remain"` +// } // // Given the input below, Other would be populated with the other // values that weren't used (everything but "name"): // -// map[string]interface{}{ -// "name": "bob", -// "address": "123 Maple St.", -// } +// map[string]interface{}{ +// "name": "bob", +// "address": "123 Maple St.", +// } // -// Omit Empty Values +// # Omit Empty Values // // When decoding from a struct to any other value, you may use the // ",omitempty" suffix on your tag to omit that value if it equates to @@ -122,37 +122,37 @@ // field value is zero and a numeric type, the field is empty, and it won't // be encoded into the destination type. // -// type Source struct { -// Age int `mapstructure:",omitempty"` -// } +// type Source struct { +// Age int `mapstructure:",omitempty"` +// } // -// Unexported fields +// # Unexported fields // // Since unexported (private) struct fields cannot be set outside the package // where they are defined, the decoder will simply skip them. // // For this output type definition: // -// type Exported struct { -// private string // this unexported field will be skipped -// Public string -// } +// type Exported struct { +// private string // this unexported field will be skipped +// Public string +// } // // Using this map as input: // -// map[string]interface{}{ -// "private": "I will be ignored", -// "Public": "I made it through!", -// } +// map[string]interface{}{ +// "private": "I will be ignored", +// "Public": "I made it through!", +// } // // The following struct will be decoded: // -// type Exported struct { -// private: "" // field is left with an empty string (zero value) -// Public: "I made it through!" -// } +// type Exported struct { +// private: "" // field is left with an empty string (zero value) +// Public: "I made it through!" +// } // -// Other Configuration +// # Other Configuration // // mapstructure is highly configurable. See the DecoderConfig struct // for other features and options that are supported. @@ -273,6 +273,17 @@ type DecoderConfig struct { // field name or tag. Defaults to `strings.EqualFold`. This can be used // to implement case-sensitive tag values, support snake casing, etc. MatchName func(mapKey, fieldName string) bool + + // TagValueRewrite is the function used to rewrite the struct tag values + // when decoding a struct to a map. Defaults to nil (no rewriting). Eg, + // + // type X struct { + // FieldName int `mapstructure:"field_name,omitempty"` + // } + // + // the tag value "field_name" will be rewritten by this function and + // the result new value will be used as the key name in the decoded map. + TagValueRewrite func(oldValue string) (newValue string) } // A Decoder takes a raw interface value and turns it into structured @@ -967,6 +978,10 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re keyName = tagValue } + if d.config.TagValueRewrite != nil { + keyName = d.config.TagValueRewrite(keyName) + } + switch v.Kind() { // this is an embedded struct, so handle it differently case reflect.Struct: diff --git a/mapstructure_test.go b/mapstructure_test.go index d31129d7..37310aec 100644 --- a/mapstructure_test.go +++ b/mapstructure_test.go @@ -8,6 +8,8 @@ import ( "strings" "testing" "time" + + "github.com/iancoleman/strcase" ) type Basic struct { @@ -2692,6 +2694,46 @@ func TestDecoder_MatchName(t *testing.T) { } } +func TestDecoder_TagValueRewrite(t *testing.T) { + type Source struct { + SnakeCase int `mapstructure:"snake_case"` + SubField *Source `mapstructure:"sub_field,omitempty"` + } + + input := Source{ + SnakeCase: 42, + SubField: &Source{ + SnakeCase: 2719, + }, + } + expected := map[string]any{ + "snakeCase": 42, + "subField": map[string]any{ + "snakeCase": 2719, + }, + } + + var actual map[string]any + config := &DecoderConfig{ + Result: &actual, + TagValueRewrite: strcase.ToLowerCamel, + } + + decoder, err := NewDecoder(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = decoder.Decode(input) + if err != nil { + t.Fatalf("err: %s", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("Decode() expected: %#v, got: %#v", expected, actual) + } +} + func TestDecoder_IgnoreUntaggedFields(t *testing.T) { type Input struct { UntaggedNumber int