Repository: openconfig/ygot
Pull request #661 (2022-05-04 02:00:14)

Add a new JSON unmarshal option that prefers the shadow path values.
It reuses the same `ytypes.PreferShadowPath` currently used as a `ytypes.SetNodeOpt`.

Testing:
* unit tests for leaf, leaf-list, container, list
* integration test in schema_tests/
  * Improved the integration test (bgp-example-*.json) so that the "ebgp" and "ibgp" containers now contain either "config" and "state" values (see [bgp-example-opstate-with-shadow.json](https://github.com/openconfig/ygot/compare/unmarshal-prefershadowpath?expand=1#diff-872186bfbb32435f77539e6b4ce1abea985dd74c52a09ae60e4c88e349134e99)), such that depending on the toggle value of `preferShadowPath` either one is unmarshalled. This is what makes [bgp-example-opstate-prefershadow.json](https://github.com/openconfig/ygot/compare/unmarshal-prefershadowpath?expand=1#diff-e6b6c244dfae7c365aaf2c4f7c1c01ae1d43dbeea30e881ca35c763dd46c54d7) different from [bgp-example-opstate.json](https://github.com/openconfig/ygot/compare/unmarshal-prefershadowpath?expand=1#diff-9bc38bff750fb321ac17e470e8b4f36f96d0fbca9966d38c2476e546c3b1db2b). The other difference between these two files is that these containers under IPV4_UNICAST store different values, which already existed prior to change.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/48813815/badge)](https://coveralls.io/builds/48813815)

Coverage decreased (-0.04%) to 90.315% when pulling **2e05569dd16640a9693a1b661709edb192131e5a on unmarshal-prefershadowpath** into **0b3574b7b52d167049bb85e4cd0684a74316eded on master**.

wenovus: @robshakir Thanks for reviewing #662, this PR is the prerequisite to it, pinging in case you missed it, thanks!

Patch:
diff --git a/util/path.go b/util/path.go
index 51e1eb3..b571656 100644
--- a/util/path.go
+++ b/util/path.go
@@ -65,9 +65,53 @@ func ShadowSchemaPaths(f reflect.StructField) [][]string {
 // leafref; the schema *yang.Entry for the field is given by
 // schema.Dir["config"].Dir["a"].
 func RelativeSchemaPath(f reflect.StructField) ([]string, error) {
-	pathTag, ok := f.Tag.Lookup("path")
-	if !ok || pathTag == "" {
-		return nil, fmt.Errorf("field %s did not specify a path", f.Name)
+	return relativeSchemaPath(f, false)
+}
+
+// RelativeSchemaPathPreferShadow returns a shadow path (if exists) or path to
+// the schema for the struct field f.
+//
+// Paths are embedded in the "shadow-path" and "path" struct tags and can be
+// either simple:
+//   e.g. "path:a"
+// or composite (if path compression is used) e.g.
+//   e.g. "path:config/a|a"
+// In the latter case, this function returns {"config", "a"}, because only the
+// longer path exists in the data tree and we want the schema for that node.
+// This case is found in OpenConfig leaf-ref cases where the key of a list is a
+// leafref; the schema *yang.Entry for the field is given by
+// schema.Dir["config"].Dir["a"].
+func RelativeSchemaPathPreferShadow(f reflect.StructField) ([]string, error) {
+	return relativeSchemaPath(f, true)
+}
+
+// relativeSchemaPath returns a path to the schema for the struct field f.
+// Paths are embedded in the "path" struct tag and can be either simple:
+//   e.g. "path:a"
+// or composite (if path compression is used) e.g.
+//   e.g. "path:config/a|a"
+// In the latter case, this function returns {"config", "a"}, because only the
+// longer path exists in the data tree and we want the schema for that node.
+// This case is found in OpenConfig leaf-ref cases where the key of a list is a
+// leafref; the schema *yang.Entry for the field is given by
+// schema.Dir["config"].Dir["a"].
+//
+// If preferShadowPath is false, the path values from the "path" tag are used.
+// If preferShadowPath is true and the field has a "shadow-path" tag, then the
+// path values from the "shadow-path" tag are used; if the field doesn't have
+// the "shadow-path" tag, then the path values from the "path" tag are used.
+func relativeSchemaPath(f reflect.StructField, preferShadowPath bool) ([]string, error) {
+	var pathTag string
+	var ok bool
+	if preferShadowPath {
+		pathTag, ok = f.Tag.Lookup("shadow-path")
+	}
+	if !ok {
+		if pathTag, ok = f.Tag.Lookup("path"); !ok || pathTag == "" {
+			return nil, fmt.Errorf("field %s did not specify a path", f.Name)
+		}
+	} else if pathTag == "" {
+		return nil, fmt.Errorf("field %s did not specify a shadow-path", f.Name)
 	}
 
 	paths := strings.Split(pathTag, "|")
diff --git a/util/reflect.go b/util/reflect.go
index a91de71..777f633 100644
--- a/util/reflect.go
+++ b/util/reflect.go
@@ -458,9 +458,32 @@ func DeepEqualDerefPtrs(a, b interface{}) bool {
 // if the struct tag is invalid, or nil if tag is valid but the schema is not
 // found in the tree at the specified path.
 func ChildSchema(schema *yang.Entry, f reflect.StructField) (*yang.Entry, error) {
+	return childSchema(schema, f, false)
+}
+
+// ChildSchemaPreferShadow returns the shadow schema (if exists) or schema for
+// the struct field f, if f contains a valid "shadow-path" or "path" tag and
+// the schema path is found in the schema tree. It returns an error if the
+// struct tag is invalid, or nil if tag is valid but the schema is not found in
+// the tree at the specified path.
+func ChildSchemaPreferShadow(schema *yang.Entry, f reflect.StructField) (*yang.Entry, error) {
+	return childSchema(schema, f, true)
+}
+
+// childSchema returns the schema for the struct field f, if f contains a valid
+// path tag and the schema path is found in the schema tree. It returns an error
+// if the struct tag is invalid, or nil if tag is valid but the schema is not
+// found in the tree at the specified path.
+//
+// If preferShadowPath is false, the path values from the "path" tag are used.
+// If preferShadowPath is true and the field has a "shadow-path" tag, then the
+// path values from the "shadow-path" tag are used; if the field doesn't have
+// the "shadow-path" tag, then the path values from the "path" tag are used.
+func childSchema(schema *yang.Entry, f reflect.StructField, preferShadowPath bool) (*yang.Entry, error) {
 	pathTag, _ := f.Tag.Lookup("path")
-	DbgSchema("childSchema for schema %s, field %s, tag %s\n", schema.Name, f.Name, pathTag)
-	p, err := RelativeSchemaPath(f)
+	shadowPathTag, _ := f.Tag.Lookup("shadow-path")
+	DbgSchema("childSchema for schema %s, field %s, path tag %s, shadow-path tag\n", schema.Name, f.Name, pathTag, shadowPathTag)
+	p, err := relativeSchemaPath(f, preferShadowPath)
 	if err != nil {
 		return nil, err
 	}
diff --git a/ytypes/container.go b/ytypes/container.go
index 2d9d50c..2609864 100644
--- a/ytypes/container.go
+++ b/ytypes/container.go
@@ -171,7 +171,11 @@ func unmarshalStruct(schema *yang.Entry, parent interface{}, jsonTree map[string
 			continue
 		}
 
-		cschema, err := util.ChildSchema(schema, ft)
+		childSchemaFn := util.ChildSchema
+		if hasPreferShadowPath(opts) {
+			childSchemaFn = util.ChildSchemaPreferShadow
+		}
+		cschema, err := childSchemaFn(schema, ft)
 		if err != nil {
 			return err
 		}
@@ -202,7 +206,7 @@ func unmarshalStruct(schema *yang.Entry, parent interface{}, jsonTree map[string
 		}
 		allSchemaPaths = append(allSchemaPaths, ssp...)
 
-		jsonValue, err := getJSONTreeValForField(schema, cschema, ft, jsonTree)
+		jsonValue, err := getJSONTreeValForField(schema, cschema, ft, jsonTree, hasPreferShadowPath(opts))
 		if err != nil {
 			return err
 		}
diff --git a/ytypes/leaf.go b/ytypes/leaf.go
index a6a2617..2091281 100644
--- a/ytypes/leaf.go
+++ b/ytypes/leaf.go
@@ -336,7 +336,7 @@ type Binary []byte
 // unmarshalLeaf unmarshals a scalar value (determined by json.Unmarshal) into
 // the parent containing the leaf.
 //   schema points to the schema for the leaf type.
-func unmarshalLeaf(inSchema *yang.Entry, parent interface{}, value interface{}, enc Encoding) error {
+func unmarshalLeaf(inSchema *yang.Entry, parent interface{}, value interface{}, enc Encoding, opts ...UnmarshalOpt) error {
 	if util.IsValueNil(value) {
 		if enc == JSONEncoding {
 			return nil
@@ -351,7 +351,7 @@ func unmarshalLeaf(inSchema *yang.Entry, parent interface{}, value interface{},
 
 	util.DbgPrint("unmarshalLeaf value %v, type %T, into parent type %T, schema name %s", util.ValueStrDebug(value), value, parent, inSchema.Name)
 
-	fieldName, _, err := schemaToStructFieldName(inSchema, parent)
+	fieldName, _, err := schemaToStructFieldName(inSchema, parent, hasPreferShadowPath(opts))
 	if err != nil {
 		return err
 	}
diff --git a/ytypes/leaf_list.go b/ytypes/leaf_list.go
index 3a57f26..cb2f361 100644
--- a/ytypes/leaf_list.go
+++ b/ytypes/leaf_list.go
@@ -88,7 +88,7 @@ func validateLeafListSchema(schema *yang.Entry) error {
 //   enc is the encoding type used to encode the value
 //   value is a JSON array if enc is JSONEncoding, represented as Go slice
 //   value is a gNMI TypedValue if enc is GNMIEncoding, represented as TypedValue_LeafListVal
-func unmarshalLeafList(schema *yang.Entry, parent interface{}, value interface{}, enc Encoding) error {
+func unmarshalLeafList(schema *yang.Entry, parent interface{}, value interface{}, enc Encoding, opts ...UnmarshalOpt) error {
 	if util.IsValueNil(value) {
 		if enc == JSONEncoding {
 			return nil
@@ -100,7 +100,7 @@ func unmarshalLeafList(schema *yang.Entry, parent interface{}, value interface{}
 		return err
 	}
 
-	fieldName, _, err := schemaToStructFieldName(schema, parent)
+	fieldName, _, err := schemaToStructFieldName(schema, parent, hasPreferShadowPath(opts))
 	if err != nil {
 		return err
 	}
@@ -127,7 +127,7 @@ func unmarshalLeafList(schema *yang.Entry, parent interface{}, value interface{}
 		// A new leaf-list update specifies the entire leaf-list, so we should clear its contents if it is non-nil.
 		clearSliceField(parent, fieldName)
 		for _, v := range sa.LeaflistVal.GetElement() {
-			if err := unmarshalGeneric(&leafSchema, parent, v, enc); err != nil {
+			if err := unmarshalGeneric(&leafSchema, parent, v, enc, opts...); err != nil {
 				return err
 			}
 		}
@@ -140,7 +140,7 @@ func unmarshalLeafList(schema *yang.Entry, parent interface{}, value interface{}
 		// A new leaf-list update specifies the entire leaf-list, so we should clear its contents if it is non-nil.
 		clearSliceField(parent, fieldName)
 		for _, leaf := range leafList {
-			if err := unmarshalGeneric(&leafSchema, parent, leaf, enc); err != nil {
+			if err := unmarshalGeneric(&leafSchema, parent, leaf, enc, opts...); err != nil {
 				return err
 			}
 		}
diff --git a/ytypes/unmarshal.go b/ytypes/unmarshal.go
index b28e8e5..306aeaa 100644
--- a/ytypes/unmarshal.go
+++ b/ytypes/unmarshal.go
@@ -39,6 +39,10 @@ type IgnoreExtraFields struct{}
 // IsUnmarshalOpt marks IgnoreExtraFields as a valid UnmarshalOpt.
 func (*IgnoreExtraFields) IsUnmarshalOpt() {}
 
+// IsUnmarshalOpt marks PreferShadowPath as a valid UnmarshalOpt.
+// See PreferShadowPath's definition in node.go.
+func (*PreferShadowPath) IsUnmarshalOpt() {}
+
 // Unmarshal recursively unmarshals JSON data tree in value into the given
 // parent, using the given schema. Any values already in the parent that are
 // not present in value are preserved. If provided schema is a leaf or leaf
@@ -85,9 +89,9 @@ func unmarshalGeneric(schema *yang.Entry, parent interface{}, value interface{},
 
 	switch {
 	case schema.IsLeaf():
-		return unmarshalLeaf(schema, parent, value, enc)
+		return unmarshalLeaf(schema, parent, value, enc, opts...)
 	case schema.IsLeafList():
-		return unmarshalLeafList(schema, parent, value, enc)
+		return unmarshalLeafList(schema, parent, value, enc, opts...)
 	case schema.IsList():
 		return unmarshalList(schema, parent, value, enc, opts...)
 	case schema.IsChoice():
@@ -108,3 +112,14 @@ func hasIgnoreExtraFields(opts []UnmarshalOpt) bool {
 	}
 	return false
 }
+
+// hasPreferShadowPath determines whether the supplied slice of UnmarshalOpts
+// contains the PreferShadowPath option.
+func hasPreferShadowPath(opts []UnmarshalOpt) bool {
+	for _, o := range opts {
+		if _, ok := o.(*PreferShadowPath); ok {
+			return true
+		}
+	}
+	return false
+}
diff --git a/ytypes/util_json.go b/ytypes/util_json.go
index a9894ee..764e953 100644
--- a/ytypes/util_json.go
+++ b/ytypes/util_json.go
@@ -27,10 +27,20 @@ import (
 // If no such JSON subtree exists, it returns nil, nil.
 // If more than one path has a JSON subtree, the function returns an error if
 // the two subtrees are unequal.
-func getJSONTreeValForField(parentSchema, schema *yang.Entry, f reflect.StructField, tree interface{}) (interface{}, error) {
-	ps, err := dataTreePaths(parentSchema, schema, f)
-	if err != nil {
-		return nil, err
+// If preferShadowPath=true, then the shadow-path tag is examined first for the
+// matching subtree.
+func getJSONTreeValForField(parentSchema, schema *yang.Entry, f reflect.StructField, tree interface{}, preferShadowPath bool) (interface{}, error) {
+	var ps [][]string
+	var err error
+	if preferShadowPath {
+		if ps, err = shadowDataTreePaths(parentSchema, schema, f); err != nil {
+			return nil, err
+		}
+	}
+	if len(ps) == 0 {
+		if ps, err = dataTreePaths(parentSchema, schema, f); err != nil {
+			return nil, err
+		}
 	}
 	var out interface{}
 	var outPath []string
diff --git a/ytypes/util_schema.go b/ytypes/util_schema.go
index e1c85d3..45abfb7 100644
--- a/ytypes/util_schema.go
+++ b/ytypes/util_schema.go
@@ -283,9 +283,11 @@ func checkDataTreeAgainstPaths(jsonTree map[string]interface{}, dataPaths [][]st
 
 // schemaToStructFieldName returns the string name of the field, which must be
 // contained in parent (a struct ptr), given the schema for the field.
+// If preferShadowPath=true, then the shadow-path tag is examined first for the
+// matching field.
 // It returns empty string and nil error if the field does not exist in the
 // parent struct.
-func schemaToStructFieldName(schema *yang.Entry, parent interface{}) (string, *yang.Entry, error) {
+func schemaToStructFieldName(schema *yang.Entry, parent interface{}, preferShadowPath bool) (string, *yang.Entry, error) {
 
 	v := reflect.ValueOf(parent)
 	if util.IsNilOrInvalidValue(v) {
@@ -305,7 +307,11 @@ func schemaToStructFieldName(schema *yang.Entry, parent interface{}) (string, *y
 	for i := 0; i < t.NumField(); i++ {
 		f := t.Field(i)
 		fieldName := f.Name
-		p, err := util.RelativeSchemaPath(f)
+		relativeSchemaPathFn := util.RelativeSchemaPath
+		if preferShadowPath {
+			relativeSchemaPathFn = util.RelativeSchemaPathPreferShadow
+		}
+		p, err := relativeSchemaPathFn(f)
 		if err != nil {
 			return "", nil, err
 		}


Test patch:
diff --git a/util/path_test.go b/util/path_test.go
index 29de27e..3576e47 100644
--- a/util/path_test.go
+++ b/util/path_test.go
@@ -28,20 +28,29 @@ import (
 
 func TestRelativeSchemaPath(t *testing.T) {
 	tests := []struct {
-		desc      string
-		fieldName string
-		want      []string
-		wantErr   string
+		desc                 string
+		fieldName            string
+		want                 []string
+		wantPreferShadowPath []string
+		wantErr              string
 	}{
 		{
-			desc:      "Good",
-			fieldName: "Good",
-			want:      []string{"config", "a"},
+			desc:                 "Good",
+			fieldName:            "Good",
+			want:                 []string{"config", "a"},
+			wantPreferShadowPath: []string{"config", "a"},
+		},
+		{
+			desc:                 "Single",
+			fieldName:            "Single",
+			want:                 []string{"a"},
+			wantPreferShadowPath: []string{"a"},
 		},
 		{
-			desc:      "Single",
-			fieldName: "Single",
-			want:      []string{"a"},
+			desc:                 "Both path and shadow-path",
+			fieldName:            "Both",
+			want:                 []string{"config", "a"},
+			wantPreferShadowPath: []string{"state", "a"},
 		},
 		{
 			desc:      "NoPath",
@@ -58,22 +67,28 @@ func TestRelativeSchemaPath(t *testing.T) {
 	pct := reflect.TypeOf(PathContainerType{})
 
 	for _, tt := range tests {
-		t.Run(tt.desc, func(t *testing.T) {
+		testcase := func(preferShadowPath bool, want []string) {
 			ft, ok := pct.FieldByName(tt.fieldName)
 			if !ok {
 				t.Fatal("could not find field A")
 			}
-			sp, err := RelativeSchemaPath(ft)
+			sp, err := relativeSchemaPath(ft, preferShadowPath)
 			if got, want := errToString(err), tt.wantErr; got != want {
 				t.Errorf("error: %s, want error: %s", got, want)
 			}
 			testErrLog(t, tt.desc, err)
 			if err == nil {
-				got, want := sp, tt.want
+				got, want := sp, want
 				if diff := cmp.Diff(want, got); diff != "" {
 					t.Errorf("(-want, +got):\n%s", diff)
 				}
 			}
+		}
+		t.Run(tt.desc, func(t *testing.T) {
+			testcase(false, tt.want)
+		})
+		t.Run(tt.desc+"_shadowpath", func(t *testing.T) {
+			testcase(true, tt.wantPreferShadowPath)
 		})
 	}
 }
@@ -84,6 +99,7 @@ type PathContainerType struct {
 	Single    *int32 `path:"a"`
 	NoPath    *int32
 	EmptyPath *int32 `path:""`
+	Both      *int32 `path:"a|config/a" shadow-path:"a|state/a"`
 }
 
 // IsYANGGoStruct implements the GoStruct interface method.
diff --git a/util/reflect_test.go b/util/reflect_test.go
index 2492438..661ce92 100644
--- a/util/reflect_test.go
+++ b/util/reflect_test.go
@@ -1605,7 +1605,7 @@ func (*ListElemStruct3) IsYANGGoStruct() {}
 
 // ContainerStruct3 is a container type for testing.
 type ContainerStruct3 struct {
-	StructKeyList map[EnumType]*ListElemStruct3 `path:"simple-key-list"`
+	StructKeyList map[EnumType]*ListElemStruct3 `path:"simple-key-list" shadow-path:"shadow-simple-key-list"`
 }
 
 // IsYANGGoStruct implements the GoStruct interface method.
@@ -1730,6 +1730,25 @@ func multipathSchema() (*yang.Entry, *yang.Entry) {
 					},
 				},
 			},
+			"shadow-simple-key-list": {
+				Name:     "shadow-simple-key-list",
+				Kind:     yang.DirectoryEntry,
+				ListAttr: yang.NewDefaultListAttr(),
+				Key:      "enum-key",
+				Config:   yang.TSTrue,
+				Dir: map[string]*yang.Entry{
+					"enum-key": {
+						Name: "enum-key",
+						Kind: yang.LeafEntry,
+						Type: &yang.YangType{Kind: yang.Yenum},
+					},
+					"value": {
+						Name: "value",
+						Kind: yang.LeafEntry,
+						Type: &yang.YangType{Kind: yang.Ystring},
+					},
+				},
+			},
 		},
 	}
 	return containerWithLeafListSchema, containerWithEnumSchema
@@ -1797,16 +1816,23 @@ func TestChildSchema(t *testing.T) {
 	}
 
 	tests := []struct {
-		desc      string
-		inSchema  *yang.Entry
-		inField   reflect.StructField
-		wantEntry *yang.Entry
-		wantErr   bool
+		desc               string
+		inSchema           *yang.Entry
+		inField            reflect.StructField
+		inPreferShadowPath bool
+		wantEntry          *yang.Entry
+		wantErr            bool
 	}{{
 		desc:      "basic",
 		inSchema:  containerWithEnumSchema,
 		inField:   simpleKeyListField3,
 		wantEntry: containerWithEnumSchema.Dir["simple-key-list"],
+	}, {
+		desc:               "basic shadowpath",
+		inSchema:           containerWithEnumSchema,
+		inField:            simpleKeyListField3,
+		inPreferShadowPath: true,
+		wantEntry:          containerWithEnumSchema.Dir["shadow-simple-key-list"],
 	}, {
 		desc:      "longpath",
 		inSchema:  containerWithLeafListSchema,
@@ -1826,7 +1852,7 @@ func TestChildSchema(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.desc, func(t *testing.T) {
-			gotEntry, gotErr := ChildSchema(tt.inSchema, tt.inField)
+			gotEntry, gotErr := childSchema(tt.inSchema, tt.inField, tt.inPreferShadowPath)
 			if (gotErr != nil) != tt.wantErr {
 				t.Fatalf("gotErr: %v, wantErr: %v", gotErr, tt.wantErr)
 			}
diff --git a/ytypes/container_test.go b/ytypes/container_test.go
index ffa00b8..5b58f55 100644
--- a/ytypes/container_test.go
+++ b/ytypes/container_test.go
@@ -481,6 +481,22 @@ func TestUnmarshalContainer(t *testing.T) {
 			json:   `{ "container-field": { "leaf2-field": 43, "config": { "leaf1-field": 42 } } }`,
 			want:   &ParentContainerStructPreferState{ContainerField: &ContainerStructPreferState{Leaf2Field: ygot.Int32(43)}},
 		},
+		{
+			desc:   "success ignoring path with preferShadowPath",
+			schema: containerSchema,
+			parent: &ParentContainerStructPreferState{},
+			json:   `{ "container-field": { "leaf2-field": 43, "state": { "leaf1-field": 42 } } }`,
+			opts:   []UnmarshalOpt{&PreferShadowPath{}},
+			want:   &ParentContainerStructPreferState{ContainerField: &ContainerStructPreferState{Leaf2Field: ygot.Int32(43)}},
+		},
+		{
+			desc:   "success unmarshalling shadow path",
+			schema: containerSchema,
+			parent: &ParentContainerStructPreferState{},
+			json:   `{ "container-field": { "leaf2-field": 43, "config": { "leaf1-field": 42 } } }`,
+			opts:   []UnmarshalOpt{&PreferShadowPath{}},
+			want:   &ParentContainerStructPreferState{ContainerField: &ContainerStructPreferState{Leaf1Field: ygot.Int32(42), Leaf2Field: ygot.Int32(43)}},
+		},
 		{
 			desc:    "fail ignoring config without shadow-path",
 			schema:  containerSchema,
diff --git a/ytypes/leaf_list_test.go b/ytypes/leaf_list_test.go
index c783432..26164ed 100644
--- a/ytypes/leaf_list_test.go
+++ b/ytypes/leaf_list_test.go
@@ -482,17 +482,44 @@ func TestUnmarshalLeafListJSONEncoding(t *testing.T) {
 				ListAttr: yang.NewDefaultListAttr(),
 				Type:     &yang.YangType{Kind: yang.Yenum},
 			},
+			"state": {
+				Name: "state",
+				Kind: yang.DirectoryEntry,
+				Dir: map[string]*yang.Entry{
+					"inner-enum-leaf-list": {
+						Name:     "inner-enum-leaf-list",
+						Kind:     yang.LeafEntry,
+						ListAttr: yang.NewDefaultListAttr(),
+						Type:     &yang.YangType{Kind: yang.Yenum},
+					},
+				},
+			},
+			"config": {
+				Name: "config",
+				Kind: yang.DirectoryEntry,
+				Dir: map[string]*yang.Entry{
+					"inner-enum-leaf-list": {
+						Name:     "inner-enum-leaf-list",
+						Kind:     yang.LeafEntry,
+						ListAttr: yang.NewDefaultListAttr(),
+						Type:     &yang.YangType{Kind: yang.Yenum},
+					},
+				},
+			},
 		},
 	}
+	addParents(containerWithLeafListSchema)
 	type ContainerStruct struct {
-		Int32LeafList []*int32   `path:"int32-leaf-list"`
-		EnumLeafList  []EnumType `path:"enum-leaf-list"`
+		Int32LeafList     []*int32   `path:"int32-leaf-list"`
+		EnumLeafList      []EnumType `path:"enum-leaf-list"`
+		InnerEnumLeafList []EnumType `path:"state/inner-enum-leaf-list" shadow-path:"config/inner-enum-leaf-list"`
 	}
 
 	tests := []struct {
 		desc    string
 		json    string
 		in      ContainerStruct
+		opts    []UnmarshalOpt
 		want    ContainerStruct
 		wantErr string
 	}{
@@ -517,6 +544,28 @@ func TestUnmarshalLeafListJSONEncoding(t *testing.T) {
 			json: `{ "enum-leaf-list" : ["E_VALUE_FORTY_TWO"] }`,
 			want: ContainerStruct{EnumLeafList: []EnumType{42}},
 		},
+		{
+			desc: "inner enum success",
+			json: `{ "state" : { "inner-enum-leaf-list" : ["E_VALUE_FORTY_TWO"] } }`,
+			want: ContainerStruct{InnerEnumLeafList: []EnumType{42}},
+		},
+		{
+			desc: "inner enum success ignoring shadow path",
+			json: `{ "config" : { "inner-enum-leaf-list" : ["E_VALUE_FORTY_TWO"] } }`,
+			want: ContainerStruct{},
+		},
+		{
+			desc: "inner enum success ignoring path with preferShadowPath",
+			json: `{ "state" : { "inner-enum-leaf-list" : ["E_VALUE_FORTY_TWO"] } }`,
+			opts: []UnmarshalOpt{&PreferShadowPath{}},
+			want: ContainerStruct{},
+		},
+		{
+			desc: "inner enum shadow path success",
+			json: `{ "config" : { "inner-enum-leaf-list" : ["E_VALUE_FORTY_TWO"] } }`,
+			opts: []UnmarshalOpt{&PreferShadowPath{}},
+			want: ContainerStruct{InnerEnumLeafList: []EnumType{42}},
+		},
 		{
 			desc:    "bad field name",
 			json:    `{ "bad field" : [42] }`,
@@ -538,7 +587,7 @@ func TestUnmarshalLeafListJSONEncoding(t *testing.T) {
 				}
 			}
 
-			err := Unmarshal(containerWithLeafListSchema, &tt.in, jsonTree)
+			err := Unmarshal(containerWithLeafListSchema, &tt.in, jsonTree, tt.opts...)
 			if got, want := errToString(err), tt.wantErr; got != want {
 				t.Errorf("%s: Unmarshal got error: %v, want error: %v", tt.desc, got, want)
 			}
diff --git a/ytypes/leaf_test.go b/ytypes/leaf_test.go
index ebe999c..281c493 100644
--- a/ytypes/leaf_test.go
+++ b/ytypes/leaf_test.go
@@ -1095,10 +1095,12 @@ func (*LeafContainerStruct) To_UnionLeafTypeSimple(i interface{}) (UnionLeafType
 
 func TestUnmarshalLeafJSONEncoding(t *testing.T) {
 	tests := []struct {
-		desc    string
-		json    string
-		want    LeafContainerStruct
-		wantErr string
+		desc              string
+		json              string
+		reverseShadowPath bool
+		opts              []UnmarshalOpt
+		want              LeafContainerStruct
+		wantErr           string
 	}{
 		{
 			desc: "nil success",
@@ -1120,6 +1122,18 @@ func TestUnmarshalLeafJSONEncoding(t *testing.T) {
 			json: `{"state" : { "inner-int8-leaf" : -42} }`,
 			want: LeafContainerStruct{},
 		},
+		{
+			desc: "config int8 ignoring path with preferShadowPath",
+			json: `{"config" : { "inner-int8-leaf" : -42} }`,
+			opts: []UnmarshalOpt{&PreferShadowPath{}},
+			want: LeafContainerStruct{},
+		},
+		{
+			desc: "state int8 success with shadow path",
+			json: `{"state" : { "inner-int8-leaf" : -42} }`,
+			opts: []UnmarshalOpt{&PreferShadowPath{}},
+			want: LeafContainerStruct{Int8LeafConfig: ygot.Int8(-42)},
+		},
 		{
 			desc:    "non-existent state fail ignoring",
 			json:    `{"state" : { "non-existent-leaf" : -42} }`,
@@ -1650,7 +1664,7 @@ func TestUnmarshalLeafJSONEncoding(t *testing.T) {
 				t.Fatalf("%s : %s", tt.desc, err)
 			}
 
-			err := Unmarshal(containerSchema, &parent, jsonTree)
+			err := Unmarshal(containerSchema, &parent, jsonTree, tt.opts...)
 			if got, want := errToString(err), tt.wantErr; got != want {
 				t.Errorf("%s (#%d): Unmarshal got error: %v, want error: %v", tt.desc, idx, got, want)
 			}
diff --git a/ytypes/list_test.go b/ytypes/list_test.go
index 88c8365..c48d940 100644
--- a/ytypes/list_test.go
+++ b/ytypes/list_test.go
@@ -630,6 +630,29 @@ func TestUnmarshalKeyedList(t *testing.T) {
 			parent: &ContainerStructPreferConfig{},
 			want:   &ContainerStructPreferConfig{},
 		},
+		{
+			desc:   "success ignoring path with preferShadowPath",
+			json:   `{ "config": { "key-list" : [ { "key" : "forty-two", "leaf-field" : 42} ] } }`,
+			opts:   []UnmarshalOpt{&PreferShadowPath{}},
+			schema: containerWithPreferConfigSchema,
+			parent: &ContainerStructPreferConfig{},
+			want:   &ContainerStructPreferConfig{},
+		},
+		{
+			desc:   "success unmarshalling shadow path",
+			json:   `{ "state": { "key-list" : [ { "key" : "forty-two", "leaf-field" : 42} ] } }`,
+			opts:   []UnmarshalOpt{&PreferShadowPath{}},
+			schema: containerWithPreferConfigSchema,
+			parent: &ContainerStructPreferConfig{},
+			want: &ContainerStructPreferConfig{
+				KeyList: map[string]*ListElemStruct{
+					"forty-two": {
+						Key:       ygot.String("forty-two"),
+						LeafField: ygot.Int32(42),
+					},
+				},
+			},
+		},
 		{
 			desc:    "bad field",
 			json:    `{ "key-list" : [ { "key" : "forty-two", "bad-field" : 42} ] }`,
diff --git a/ytypes/schema_tests/testdata/bgp-example-opstate-prefershadow.json b/ytypes/schema_tests/testdata/bgp-example-opstate-prefershadow.json
new file mode 100644
index 0000000..1414a7f
--- /dev/null
+++ b/ytypes/schema_tests/testdata/bgp-example-opstate-prefershadow.json
@@ -0,0 +1,266 @@
+{
+  "openconfig-network-instance:network-instances": {
+    "network-instance": [
+      {
+        "name": "DEFAULT",
+        "state": {
+          "name": "DEFAULT"
+        },
+        "protocols": {
+          "protocol": [
+            {
+              "name": "BGP",
+              "identifier": "openconfig-policy-types:BGP",
+              "state": {
+                "name": "BGP",
+                "identifier": "openconfig-policy-types:BGP"
+              },
+              "bgp": {
+                "global": {
+                  "afi-safis": {
+                    "afi-safi": [
+                      {
+                        "afi-safi-name": "openconfig-bgp-types:IPV4_UNICAST",
+                        "state": {
+                          "afi-safi-name": "openconfig-bgp-types:IPV4_UNICAST",
+                          "enabled": true
+                        },
+                        "use-multiple-paths": {
+                          "ebgp": {
+                            "state": {
+                              "maximum-paths": 32
+                            }
+                          },
+                          "ibgp": {
+                            "state": {
+                              "maximum-paths": 33
+                            }
+                          }
+                        }
+                      },
+                      {
+                        "afi-safi-name": "openconfig-bgp-types:IPV6_UNICAST",
+                        "state": {
+                          "afi-safi-name": "openconfig-bgp-types:IPV6_UNICAST",
+                          "enabled": true
+                        },
+                        "use-multiple-paths": {
+                          "ibgp": {
+                            "state": {
+                              "maximum-paths": 32
+                            }
+                          }
+                        }
+                      }
+                    ]
+                  },
+                  "state": {
+                    "as": 11111,
+                    "router-id": "10.10.10.10"
+                  }
+                },
+                "neighbors": {
+                  "neighbor": [
+                    {
+                      "state": {
+                        "description": "desc1",
+                        "neighbor-address": "192.168.1.2",
+                        "peer-group": "PEER_GROUP_2"
+                      },
+                      "neighbor-address": "192.168.1.2",
+                      "timers": {
+                        "state": {
+                          "restart-time": 0
+                        }
+                      },
+                      "transport": {
+                        "state": {
+                          "local-address": "192.168.1.1"
+                        }
+                      }
+                    },
+                    {
+                      "afi-safis": {
+                        "afi-safi": [
+                          {
+                            "afi-safi-name": "openconfig-bgp-types:IPV4_UNICAST",
+                            "apply-policy": {
+                              "state": {
+                                "export-policy": [
+                                  "LOOPBACK-TO-BGP"
+                                ],
+                                "import-policy": [
+                                  "PASS"
+                                ]
+                              }
+                            },
+                            "state": {
+                              "afi-safi-name": "openconfig-bgp-types:IPV4_UNICAST",
+                              "enabled": true
+                            }
+                          }
+                        ]
+                      },
+                      "state": {
+                        "description": "netlab-core1",
+                        "neighbor-address": "192.168.1.254",
+                        "peer-as": 11113
+                      },
+                      "neighbor-address": "192.168.1.254"
+                    },
+                    {
+                      "state": {
+                        "description": "desc2",
+                        "neighbor-address": "192.168.2.0",
+                        "peer-group": "PEER_GROUP_1"
+                      },
+                      "neighbor-address": "192.168.2.0",
+                      "transport": {
+                        "state": {
+                          "local-address": "192.168.2.1"
+                        }
+                      }
+                    },
+                    {
+                      "state": {
+                        "description": "desc2",
+                        "neighbor-address": "192.168.2.2",
+                        "peer-group": "PEER_GROUP_1"
+                      },
+                      "neighbor-address": "192.168.2.2",
+                      "transport": {
+                        "state": {
+                          "local-address": "192.168.2.3"
+                        }
+                      }
+                    }
+                  ]
+                },
+                "peer-groups": {
+                  "peer-group": [
+                    {
+                      "afi-safis": {
+                        "afi-safi": [
+                          {
+                            "afi-safi-name": "openconfig-bgp-types:IPV4_UNICAST",
+                            "apply-policy": {
+                              "state": {
+                                "export-policy": [
+                                  "PASS-ALL"
+                                ],
+                                "import-policy": [
+                                  "IMPORT_POLICY_1"
+                                ]
+                              }
+                            },
+                            "state": {
+                              "afi-safi-name": "openconfig-bgp-types:IPV4_UNICAST",
+                              "enabled": true
+                            },
+                            "ipv4-unicast": {
+                              "prefix-limit": {
+                                "state": {
+                                  "max-prefixes": 20000
+                                }
+                              }
+                            },
+                            "use-multiple-paths": {
+                              "state": {
+                                "enabled": true
+                              }
+                            }
+                          }
+                        ]
+                      },
+                      "state": {
+                        "peer-as": 11112,
+                        "peer-group-name": "PEER_GROUP_1"
+                      },
+                      "peer-group-name": "PEER_GROUP_1"
+                    },
+                    {
+                      "afi-safis": {
+                        "afi-safi": [
+                          {
+                            "afi-safi-name": "openconfig-bgp-types:IPV4_UNICAST",
+                            "apply-policy": {
+                              "state": {
+                                "export-policy": [
+                                  "ENTITY_TYPE_1"
+                                ],
+                                "import-policy": [
+                                  "PASS-ALL"
+                                ]
+                              }
+                            },
+                            "state": {
+                              "afi-safi-name": "openconfig-bgp-types:IPV4_UNICAST",
+                              "enabled": true
+                            },
+                            "ipv4-unicast": {
+                              "prefix-limit": {
+                                "state": {
+                                  "max-prefixes": 1000
+                                }
+                              }
+                            },
+                            "use-multiple-paths": {
+                              "state": {
+                                "enabled": true
+                              }
+                            }
+                          }
+                        ]
+                      },
+                      "state": {
+                        "peer-as": 65514,
+                        "peer-group-name": "PEER_GROUP_2"
+                      },
+                      "peer-group-name": "PEER_GROUP_2"
+                    }
+                  ]
+                }
+              }
+            }
+          ]
+        }
+      }
+    ]
+  },
+  "openconfig-routing-policy:routing-policy": {
+    "policy-definitions": {
+      "policy-definition": [
+        {
+          "state": {
+            "name": "ENTITY_TYPE_1"
+          },
+          "name": "ENTITY_TYPE_1"
+        },
+        {
+          "state": {
+            "name": "LOOPBACK-TO-BGP"
+          },
+          "name": "LOOPBACK-TO-BGP"
+        },
+        {
+          "state": {
+            "name": "IMPORT_POLICY_1"
+          },
+          "name": "IMPORT_POLICY_1"
+        },
+        {
+          "state": {
+            "name": "PASS"
+          },
+          "name": "PASS"
+        },
+        {
+          "state": {
+            "name": "PASS-ALL"
+          },
+          "name": "PASS-ALL"
+        }
+      ]
+    }
+  }
+}
diff --git a/ytypes/schema_tests/testdata/bgp-example-opstate-with-shadow.json b/ytypes/schema_tests/testdata/bgp-example-opstate-with-shadow.json
index 0f7fc30..f17bdad 100644
--- a/ytypes/schema_tests/testdata/bgp-example-opstate-with-shadow.json
+++ b/ytypes/schema_tests/testdata/bgp-example-opstate-with-shadow.json
@@ -29,6 +29,9 @@
                           "ebgp": {
                             "config": {
                               "maximum-paths": 32
+                            },
+                            "state": {
+                              "maximum-paths": 32
                             }
                           },
                           "ibgp": {
@@ -53,9 +56,6 @@
                         },
                         "use-multiple-paths": {
                           "ebgp": {
-                            "config": {
-                              "maximum-paths": 32
-                            },
                             "state": {
                               "maximum-paths": 32
                             }
@@ -63,9 +63,6 @@
                           "ibgp": {
                             "config": {
                               "maximum-paths": 32
-                            },
-                            "state": {
-                              "maximum-paths": 32
                             }
                           }
                         }
diff --git a/ytypes/schema_tests/testdata/bgp-example-opstate.json b/ytypes/schema_tests/testdata/bgp-example-opstate.json
index f1228b5..de18ef1 100644
--- a/ytypes/schema_tests/testdata/bgp-example-opstate.json
+++ b/ytypes/schema_tests/testdata/bgp-example-opstate.json
@@ -26,6 +26,11 @@
                           "enabled": true
                         },
                         "use-multiple-paths": {
+                          "ebgp": {
+                            "state": {
+                              "maximum-paths": 32
+                            }
+                          },
                           "ibgp": {
                             "state": {
                               "maximum-paths": 32
@@ -44,11 +49,6 @@
                             "state": {
                               "maximum-paths": 32
                             }
-                          },
-                          "ibgp": {
-                            "state": {
-                              "maximum-paths": 32
-                            }
                           }
                         }
                       }
diff --git a/ytypes/schema_tests/validate_test.go b/ytypes/schema_tests/validate_test.go
index 1c18374..c915113 100644
--- a/ytypes/schema_tests/validate_test.go
+++ b/ytypes/schema_tests/validate_test.go
@@ -637,6 +637,15 @@ func TestUnmarshal(t *testing.T) {
 			unmarshalFn:     opstateoc.Unmarshal,
 			outjsonFilePath: "bgp-example-opstate.json",
 		},
+		{
+			desc:         "bgp with prefer_operational_state, with schema ignoring normal paths with PreferShadowPath=true",
+			jsonFilePath: "bgp-example-opstate-with-shadow.json",
+			parent:       &opstateoc.Device{},
+			unmarshalFn:  opstateoc.Unmarshal,
+			opts:         []ytypes.UnmarshalOpt{&ytypes.PreferShadowPath{}},
+			// This JSON file is different because it unmarshalled what's in the "config" containers rather than what's in the "state" containers.
+			outjsonFilePath: "bgp-example-opstate-prefershadow.json",
+		},
 		{
 			desc:              "interfaces",
 			jsonFilePath:      "interfaces-example.json",