Repository: goccy/go-yaml
Pull request #56 (2019-12-10 09:52:11)

Add disallowUnknownField Option
disallowUnknownField option likes [encoding/json#Decoder.DisallowUnknownField](https://golang.org/pkg/encoding/json/#Decoder.DisallowUnknownFields)
I think this is useful for typo.
If you don't like this option, please close PR. Thanks.


Patch:
diff --git a/decode.go b/decode.go
index 73f96d9..3718e01 100644
--- a/decode.go
+++ b/decode.go
@@ -21,28 +21,30 @@ import (
 
 // Decoder reads and decodes YAML values from an input stream.
 type Decoder struct {
-	reader              io.Reader
-	referenceReaders    []io.Reader
-	anchorMap           map[string]ast.Node
-	opts                []DecodeOption
-	referenceFiles      []string
-	referenceDirs       []string
-	isRecursiveDir      bool
-	isResolvedReference bool
-	validator           StructValidator
+	reader               io.Reader
+	referenceReaders     []io.Reader
+	anchorMap            map[string]ast.Node
+	opts                 []DecodeOption
+	referenceFiles       []string
+	referenceDirs        []string
+	isRecursiveDir       bool
+	isResolvedReference  bool
+	validator            StructValidator
+	disallowUnknownField bool
 }
 
 // NewDecoder returns a new decoder that reads from r.
 func NewDecoder(r io.Reader, opts ...DecodeOption) *Decoder {
 	return &Decoder{
-		reader:              r,
-		anchorMap:           map[string]ast.Node{},
-		opts:                opts,
-		referenceReaders:    []io.Reader{},
-		referenceFiles:      []string{},
-		referenceDirs:       []string{},
-		isRecursiveDir:      false,
-		isResolvedReference: false,
+		reader:               r,
+		anchorMap:            map[string]ast.Node{},
+		opts:                 opts,
+		referenceReaders:     []io.Reader{},
+		referenceFiles:       []string{},
+		referenceDirs:        []string{},
+		isRecursiveDir:       false,
+		isResolvedReference:  false,
+		disallowUnknownField: false,
 	}
 }
 
@@ -407,7 +409,7 @@ func (d *Decoder) castToAssignableValue(value reflect.Value, target reflect.Type
 	return value
 }
 
-func (d *Decoder) keyToNodeMap(node ast.Node) (map[string]ast.Node, error) {
+func (d *Decoder) keyToNodeMap(node ast.Node, getKeyOrValueNode func(*ast.MapNodeIter) ast.Node) (map[string]ast.Node, error) {
 	mapNode, err := d.getMapNode(node)
 	if err != nil {
 		return nil, errors.Wrapf(err, "failed to get map node")
@@ -420,7 +422,7 @@ func (d *Decoder) keyToNodeMap(node ast.Node) (map[string]ast.Node, error) {
 	for mapIter.Next() {
 		keyNode := mapIter.Key()
 		if keyNode.Type() == ast.MergeKeyType {
-			mergeMap, err := d.keyToNodeMap(mapIter.Value())
+			mergeMap, err := d.keyToNodeMap(mapIter.Value(), getKeyOrValueNode)
 			if err != nil {
 				return nil, errors.Wrapf(err, "failed to get keyToNodeMap by MergeKey node")
 			}
@@ -432,12 +434,28 @@ func (d *Decoder) keyToNodeMap(node ast.Node) (map[string]ast.Node, error) {
 			if !ok {
 				return nil, errors.Wrapf(err, "failed to decode map key")
 			}
-			keyToNodeMap[key] = mapIter.Value()
+			keyToNodeMap[key] = getKeyOrValueNode(mapIter)
 		}
 	}
 	return keyToNodeMap, nil
 }
 
+func (d *Decoder) keyToKeyNodeMap(node ast.Node) (map[string]ast.Node, error) {
+	m, err := d.keyToNodeMap(node, func(nodeMap *ast.MapNodeIter) ast.Node { return nodeMap.Key() })
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to get keyToNodeMap")
+	}
+	return m, nil
+}
+
+func (d *Decoder) keyToValueNodeMap(node ast.Node) (map[string]ast.Node, error) {
+	m, err := d.keyToNodeMap(node, func(nodeMap *ast.MapNodeIter) ast.Node { return nodeMap.Value() })
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to get keyToNodeMap")
+	}
+	return m, nil
+}
+
 func (d *Decoder) setDefaultValueIfConflicted(v reflect.Value, fieldMap StructFieldMap) error {
 	typ := v.Type()
 	if typ.Kind() != reflect.Struct {
@@ -516,9 +534,16 @@ func (d *Decoder) decodeStruct(dst reflect.Value, src ast.Node) error {
 	if err != nil {
 		return errors.Wrapf(err, "failed to create struct field map")
 	}
-	keyToNodeMap, err := d.keyToNodeMap(src)
+	keyToNodeMap, err := d.keyToValueNodeMap(src)
 	if err != nil {
-		return errors.Wrapf(err, "failed to get keyToNodeMap")
+		return errors.Wrapf(err, "failed to get keyToValueNodeMap")
+	}
+	var unknownFields map[string]ast.Node
+	if d.disallowUnknownField {
+		unknownFields, err = d.keyToKeyNodeMap(src)
+		if err != nil {
+			return errors.Wrapf(err, "failed to get keyToKeyNodeMap")
+		}
 	}
 
 	var foundErr error
@@ -567,6 +592,7 @@ func (d *Decoder) decodeStruct(dst reflect.Value, src ast.Node) error {
 		if !exists {
 			continue
 		}
+		delete(unknownFields, structField.RenderName)
 		fieldValue := structValue.Elem().FieldByName(field.Name)
 		if fieldValue.Type().Kind() == reflect.Ptr && src.Type() == ast.NullType {
 			// set nil value to pointer
@@ -610,6 +636,11 @@ func (d *Decoder) decodeStruct(dst reflect.Value, src ast.Node) error {
 			}
 		}
 	}
+	if len(unknownFields) != 0 && d.disallowUnknownField {
+		for key, node := range unknownFields {
+			return errors.ErrSyntax(fmt.Sprintf(`unknown field "%s"`, key), node.GetToken())
+		}
+	}
 	dst.Set(structValue.Elem())
 	if foundErr != nil {
 		return errors.Wrapf(foundErr, "failed to decode value")
diff --git a/option.go b/option.go
index e6274e4..9fe7c3c 100644
--- a/option.go
+++ b/option.go
@@ -45,6 +45,16 @@ func Validator(v StructValidator) DecodeOption {
 	}
 }
 
+// DisallowUnknownField causes the Decoder to return an error when the destination
+// is a struct and the input contains object keys which do not match any
+// non-ignored, exported fields in the destination.
+func DisallowUnknownField() DecodeOption {
+	return func(d *Decoder) error {
+		d.disallowUnknownField = true
+		return nil
+	}
+}
+
 // EncodeOption functional option type for Encoder
 type EncodeOption func(e *Encoder) error
 


Test patch:
diff --git a/decode_test.go b/decode_test.go
index 31b5abd..2d882fb 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -1233,3 +1233,24 @@ bar: c
 	// 1
 	// c
 }
+
+func Example_DisallowUnknownField() {
+	var v struct {
+		A string `yaml:"simple"`
+		C string `yaml:"complicated"`
+	}
+
+	const src = `---
+simple: string
+complecated: string
+`
+	err := yaml.NewDecoder(strings.NewReader(src), yaml.DisallowUnknownField()).Decode(&v)
+	fmt.Printf("%v\n", err)
+
+	// OUTPUT:
+	// [3:1] unknown field "complecated"
+	//        1 | ---
+	//        2 | simple: string
+	//     >  3 | complecated: string
+	//           ^
+}