From a48f5a3a84831abe36692fa21c3d82b786c55841 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Wed, 13 Nov 2019 10:31:00 +0900 Subject: [PATCH 1/3] Add support for `json` struct tags --- decode_test.go | 24 ++++++++++++++++++++++++ struct.go | 9 +++++++++ 2 files changed, 33 insertions(+) diff --git a/decode_test.go b/decode_test.go index 132a6a99..020c544c 100644 --- a/decode_test.go +++ b/decode_test.go @@ -1058,3 +1058,27 @@ a: t.Logf("%s", yaml.FormatError(err, false, true)) t.Logf("%s", yaml.FormatError(err, true, true)) } + +func TestDecoder_JSONTags(t *testing.T) { + var v struct { + A string `json:"a_json"` // no YAML tag + B string `json:"b_json" yaml:"b_yaml"` // both tags + } + + const src = `--- +a_json: a_json_value +b_json: b_json_value +b_yaml: b_yaml_value +` + if err := yaml.NewDecoder(strings.NewReader(src)).Decode(&v); err != nil { + t.Fatalf(`parsing should succeed: %s`, err) + } + + if v.A != "a_json_value" { + t.Fatalf("v.A should be `a_json_value`, got `%s`", v.A) + } + + if v.B != "b_yaml_value" { + t.Fatalf("v.B should be `b_yaml_value`, got `%s`", v.B) + } +} diff --git a/struct.go b/struct.go index c4f1fb81..f0729145 100644 --- a/struct.go +++ b/struct.go @@ -26,10 +26,19 @@ type StructField struct { } func structField(field reflect.StructField) *StructField { + // If struct tag `yaml` exist, use that. If no `yaml` + // exists, but `json` does, use that and try the best to + // adhere to its rules + tag := field.Tag.Get(StructTagName) if tag == "" && strings.Index(string(field.Tag), ":") < 0 { tag = string(field.Tag) } + + if tag == "" { + tag = field.Tag.Get(`json`) + } + fieldName := strings.ToLower(field.Name) options := strings.Split(tag, ",") if len(options) > 0 { From 1dbf6f489430fdc9fde2b6fd4f7f6c34798506de Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Thu, 14 Nov 2019 09:05:31 +0900 Subject: [PATCH 2/3] refactor tag fetching, apply same rules to isIgnoredStructField --- struct.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/struct.go b/struct.go index 80a9a005..2c6015da 100644 --- a/struct.go +++ b/struct.go @@ -25,16 +25,19 @@ type StructField struct { IsInline bool } -func structField(field reflect.StructField) *StructField { +func getTag(field reflect.StructField) string { // If struct tag `yaml` exist, use that. If no `yaml` // exists, but `json` does, use that and try the best to // adhere to its rules - tag := field.Tag.Get(StructTagName) if tag == "" { tag = field.Tag.Get(`json`) } + return tag +} +func structField(field reflect.StructField) *StructField { + tag := getTag(field) fieldName := strings.ToLower(field.Name) options := strings.Split(tag, ",") if len(options) > 0 { @@ -81,7 +84,7 @@ func isIgnoredStructField(field reflect.StructField) bool { // private field return true } - tag := field.Tag.Get(StructTagName) + tag := getTag(field) if tag == "-" { return true } From 45fd347c28500ea81573092b5872233b56609a18 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Thu, 14 Nov 2019 09:12:39 +0900 Subject: [PATCH 3/3] Add documentation and example tests --- README.md | 34 ++++++++++++++++++++++++++++++++++ decode_test.go | 43 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 832c9749..772322a1 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,40 @@ if err := yaml.Unmarshal([]byte(yml), &v); err != nil { } ``` +To control marshal/unmarshal behavior, you can use the `yaml` tag + +```go + yml := `--- +foo: 1 +bar: c +` +var v struct { + A int `yaml:"foo"` + B string `yaml:"bar"` +} +if err := yaml.Unmarshal([]byte(yml), &v); err != nil { + ... +} +``` + +For convenience, we also accept the `json` tag. Note that not all options from +the `json` tag will have significance when parsing YAML documents. If both +tags exist, `yaml` tag will take precedence. + +```go + yml := `--- +foo: 1 +bar: c +` +var v struct { + A int `json:"foo"` + B string `json:"bar"` +} +if err := yaml.Unmarshal([]byte(yml), &v); err != nil { + ... +} +``` + For custom marshal/unmarshaling, implement one of Bytes or Interface Marshaler/Unmarshaler. The difference is that while BytesMarshaler/BytesUnmarshaler behave like `encoding.json`, InterfaceMarshaler/InterfaceUnmarshaler behave like `gopkg.in/yaml.v2`. Semantically both are the same, but they differ in performance. Because indentation matter in YAML, you cannot simply accept a valid YAML fragment from a Marshaler, and expect it to work when it is attached to the parent container's serialized form. Therefore when we receive use the BytesMarshaler, which returns []byte, we must decode it once to figure out how to make it work in the given context. If you use the InterfaceMarshaler, we can skip the decoding. diff --git a/decode_test.go b/decode_test.go index 9c882643..747fb2f0 100644 --- a/decode_test.go +++ b/decode_test.go @@ -3,6 +3,7 @@ package yaml_test import ( "bytes" "fmt" + "log" "math" "reflect" "strings" @@ -1061,7 +1062,7 @@ a: func TestDecoder_JSONTags(t *testing.T) { var v struct { - A string `json:"a_json"` // no YAML tag + A string `json:"a_json"` // no YAML tag B string `json:"b_json" yaml:"b_yaml"` // both tags } @@ -1082,3 +1083,43 @@ b_yaml: b_yaml_value t.Fatalf("v.B should be `b_yaml_value`, got `%s`", v.B) } } + +func Example_YAMLTags() { + yml := `--- +foo: 1 +bar: c +A: 2 +B: d +` + var v struct { + A int `yaml:"foo" json:"A"` + B string `yaml:"bar" json:"B"` + } + if err := yaml.Unmarshal([]byte(yml), &v); err != nil { + log.Fatal(err) + } + fmt.Println(v.A) + fmt.Println(v.B) + // OUTPUT: + // 1 + // c +} + +func Example_JSONTags() { + yml := `--- +foo: 1 +bar: c +` + var v struct { + A int `json:"foo"` + B string `json:"bar"` + } + if err := yaml.Unmarshal([]byte(yml), &v); err != nil { + log.Fatal(err) + } + fmt.Println(v.A) + fmt.Println(v.B) + // OUTPUT: + // 1 + // c +}