Repository: openconfig/ygot
Pull request #845 (2023-05-25 22:07:21)

Support unmarshalling JSON ordered map

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

Coverage: 89.924% (-0.03%) from 89.95% when pulling **0e2a5c017d8d49e542b1a9dc0edc8c97bf77addf on unmarshal-orderedmap-json** into **6cd5d9759a7df516262f02d571c3beaea64218d8 on master**.


Patch:
diff --git a/util/reflect.go b/util/reflect.go
index e11a32f..088da5f 100644
--- a/util/reflect.go
+++ b/util/reflect.go
@@ -188,11 +188,7 @@ func InsertIntoSlice(parentSlice interface{}, value interface{}) error {
 	return nil
 }
 
-// InsertIntoMap inserts value with key into parent which must be a map or a
-// GoOrderedList interface.
-//
-// If the type is a GoOrderedList, then there must not exist an existing
-// element.
+// InsertIntoMap inserts value with key into parent which must be a map.
 func InsertIntoMap(parentMap interface{}, key interface{}, value interface{}) error {
 	DbgPrint("InsertIntoMap into parent type %T with key %v(%T) value \n%s\n (%T)",
 		parentMap, ValueStrDebug(key), key, pretty.Sprint(value), value)
@@ -202,27 +198,43 @@ func InsertIntoMap(parentMap interface{}, key interface{}, value interface{}) er
 	kv := reflect.ValueOf(key)
 	vv := reflect.ValueOf(value)
 
-	_, isOrderedList := parentMap.(goOrderedList)
-
-	switch {
-	case t.Kind() == reflect.Map:
-		v.SetMapIndex(kv, vv)
-	case isOrderedList:
-		appendMethod := v.MethodByName("Append")
-		if !appendMethod.IsValid() || appendMethod.IsZero() {
-			return fmt.Errorf("did not find Append() method on type: %v", t.Name())
-		}
-		ret := appendMethod.Call([]reflect.Value{vv})
-		if got, wantReturnN := len(ret), 1; got != wantReturnN {
-			return fmt.Errorf("method Append() doesn't have expected number of return values, got %v, want %v", got, wantReturnN)
-		}
-		if err := ret[0].Interface(); err != nil {
-			return fmt.Errorf("unable to append new ordered map element (it is expected that YANG `ordered-by user` lists are always unmarshalled as a whole instead of individually: %v", err)
-		}
-	default:
+	if t.Kind() != reflect.Map {
 		return fmt.Errorf("InsertIntoMap parent type is %s, must be map", t)
 	}
 
+	v.SetMapIndex(kv, vv)
+
+	return nil
+}
+
+// MethodByName returns a valid method for the given value, or an error if the
+// method is not valid for use.
+func MethodByName(v reflect.Value, name string) (reflect.Value, error) {
+	method := v.MethodByName(name)
+	if !method.IsValid() || method.IsZero() {
+		return method, fmt.Errorf("did not find %s() method on type: %s", name, v.Type().Name())
+	}
+	return method, nil
+}
+
+// AppendIntoOrderedMap append value with key into parent which must be a
+// GoOrderedList interface.
+//
+// If the type is a GoOrderedList, then there must not exist an existing
+// element.
+func AppendIntoOrderedMap(orderedMap goOrderedList, value interface{}) error {
+	appendMethod, err := MethodByName(reflect.ValueOf(orderedMap), "Append")
+	if err != nil {
+		return err
+	}
+	ret := appendMethod.Call([]reflect.Value{reflect.ValueOf(value)})
+	if got, wantReturnN := len(ret), 1; got != wantReturnN {
+		return fmt.Errorf("method Append() doesn't have expected number of return values, got %v, want %v", got, wantReturnN)
+	}
+	if err := ret[0].Interface(); err != nil {
+		return fmt.Errorf("unable to append new ordered map element (it is expected that YANG `ordered-by user` lists are always unmarshalled as a whole instead of individually: %v", err)
+	}
+
 	return nil
 }
 
diff --git a/ygot/render.go b/ygot/render.go
index a52ee4c..2a3eae6 100644
--- a/ygot/render.go
+++ b/ygot/render.go
@@ -402,9 +402,10 @@ func findUpdatedOrderedListLeaves(leaves any, s GoOrderedList, parent *gnmiPath)
 
 	fval := reflect.ValueOf(s)
 	// First get the ordered keys, and then index into each of the values associated with it.
-	keysMethod := fval.MethodByName("Keys")
-	if !keysMethod.IsValid() || keysMethod.IsZero() {
-		errs.Add(fmt.Errorf("did not find Keys() method on type: %v", fval.Type().Name()))
+	keysMethod, err := util.MethodByName(fval, "Keys")
+	if err != nil {
+		errs.Add(err)
+		return errs.Err()
 	}
 	ret := keysMethod.Call(nil)
 	if got, wantReturnN := len(ret), 1; got != wantReturnN {
@@ -417,9 +418,9 @@ func findUpdatedOrderedListLeaves(leaves any, s GoOrderedList, parent *gnmiPath)
 		return errs.Err()
 	}
 
-	getMethod := fval.MethodByName("Get")
-	if !getMethod.IsValid() || getMethod.IsZero() {
-		errs.Add(fmt.Errorf("did not find Get() method on type: %v", fval.Type().Name()))
+	getMethod, err := util.MethodByName(fval, "Get")
+	if err != nil {
+		errs.Add(err)
 		return errs.Err()
 	}
 
@@ -1613,9 +1614,9 @@ func jsonValue(field reflect.Value, parentMod string, args jsonOutputConfig) (in
 	case reflect.Ptr:
 		if _, ok := field.Interface().(GoOrderedList); ok {
 			// This is an ordered-map for YANG "ordered-by user" lists.
-			valuesMethod := field.MethodByName("Values")
-			if !valuesMethod.IsValid() || valuesMethod.IsZero() {
-				return nil, fmt.Errorf("did not find Values() method on type: %v", field.Type().Name())
+			valuesMethod, err := util.MethodByName(field, "Values")
+			if err != nil {
+				return nil, err
 			}
 			ret := valuesMethod.Call(nil)
 			if got, wantReturnN := len(ret), 1; got != wantReturnN {
diff --git a/ytypes/leaf.go b/ytypes/leaf.go
index 740cf28..a199e2e 100644
--- a/ytypes/leaf.go
+++ b/ytypes/leaf.go
@@ -620,9 +620,9 @@ func getUnionTypesNotEnums(schema *yang.Entry, yt *yang.YangType) ([]*yang.YangT
 // type) for a given schema, which must be for an enum type. t is the type of
 // the containing parent struct.
 func schemaToEnumTypes(schema *yang.Entry, t reflect.Type) ([]reflect.Type, error) {
-	enumTypesMethod := reflect.New(t).Elem().MethodByName("ΛEnumTypeMap")
-	if !enumTypesMethod.IsValid() {
-		return nil, fmt.Errorf("type %s does not have a ΛEnumTypesMap function", t)
+	enumTypesMethod, err := util.MethodByName(reflect.New(t).Elem(), "ΛEnumTypeMap")
+	if err != nil {
+		return nil, err
 	}
 
 	ec := enumTypesMethod.Call(nil)
diff --git a/ytypes/list.go b/ytypes/list.go
index 7d8c8cf..3e71df7 100644
--- a/ytypes/list.go
+++ b/ytypes/list.go
@@ -22,6 +22,7 @@ import (
 	"github.com/kylelemons/godebug/pretty"
 	"github.com/openconfig/goyang/pkg/yang"
 	"github.com/openconfig/ygot/util"
+	"github.com/openconfig/ygot/ygot"
 )
 
 // Refer to: https://tools.ietf.org/html/rfc6020#section-7.8.
@@ -293,10 +294,37 @@ func unmarshalList(schema *yang.Entry, parent interface{}, jsonList interface{},
 	// Parent must be a map, slice ptr, or struct ptr.
 	t := reflect.TypeOf(parent)
 
-	if util.IsTypeStructPtr(t) {
+	orderedMap, isOrderedMap := parent.(ygot.GoOrderedList)
+
+	var listElementType reflect.Type
+	switch {
+	case !isOrderedMap && util.IsTypeStructPtr(t):
 		// May be trying to unmarshal a single list element rather than the
 		// whole list.
 		return unmarshalContainerWithListSchema(schema, parent, jsonList, opts...)
+	case !isOrderedMap:
+		if !(util.IsTypeMap(t) || util.IsTypeSlicePtr(t)) {
+			return fmt.Errorf("unmarshalList for %s got parent type %s, expect map, slice ptr or struct ptr", schema.Name, t.Kind())
+		}
+
+		listElementType = t.Elem()
+		if util.IsTypeSlicePtr(t) {
+			listElementType = t.Elem().Elem()
+		}
+		if !util.IsTypeStructPtr(listElementType) {
+			return fmt.Errorf("unmarshalList for %s parent type %T, has bad field type %v", listElementType, parent, listElementType)
+		}
+	default:
+		appendMethod, ok := t.MethodByName("Append")
+		if !ok {
+			return fmt.Errorf("did not find Append() method on type: %s", t.Name())
+		}
+		methodSpec := appendMethod.Func.Type()
+		// The receiver is the first arg.
+		if gotIn, wantIn := methodSpec.NumIn(), 2; gotIn != wantIn {
+			return fmt.Errorf("method Append() doesn't have expected number of input parameters, got %v, want %v", gotIn, wantIn)
+		}
+		listElementType = methodSpec.In(1)
 	}
 
 	// jsonList represents a JSON array, which is a Go slice.
@@ -306,18 +334,6 @@ func unmarshalList(schema *yang.Entry, parent interface{}, jsonList interface{},
 			schema.Name, util.ValueStr(jsonList), jsonList)
 	}
 
-	if !(util.IsTypeMap(t) || util.IsTypeSlicePtr(t)) {
-		return fmt.Errorf("unmarshalList for %s got parent type %s, expect map, slice ptr or struct ptr", schema.Name, t.Kind())
-	}
-
-	listElementType := t.Elem()
-	if util.IsTypeSlicePtr(t) {
-		listElementType = t.Elem().Elem()
-	}
-	if !util.IsTypeStructPtr(listElementType) {
-		return fmt.Errorf("unmarshalList for %s parent type %T, has bad field type %v", listElementType, parent, listElementType)
-	}
-
 	// Iterate over JSON list. Each JSON list element is a map with the field
 	// name as the key. The JSON values must be unmarshaled and inserted into
 	// the new struct list element. When all fields of the new element have been
@@ -336,6 +352,8 @@ func unmarshalList(schema *yang.Entry, parent interface{}, jsonList interface{},
 		}
 
 		switch {
+		case isOrderedMap:
+			err = util.AppendIntoOrderedMap(orderedMap, newVal.Interface())
 		case util.IsTypeMap(t):
 			var newKey reflect.Value
 			newKey, err = makeKeyForInsert(schema, parent, newVal)
diff --git a/ytypes/util_types.go b/ytypes/util_types.go
index 33c6edb..80cf56d 100644
--- a/ytypes/util_types.go
+++ b/ytypes/util_types.go
@@ -125,9 +125,9 @@ func castToEnumValue(ft reflect.Type, value string) (interface{}, error) {
 	}
 
 	util.DbgPrint("checking for matching enum value for type %s", ft)
-	mapMethod := reflect.New(ft).MethodByName("ΛMap")
-	if !mapMethod.IsValid() {
-		return 0, fmt.Errorf("%s does not have a ΛMap function", ft)
+	mapMethod, err := util.MethodByName(reflect.New(ft), "ΛMap")
+	if err != nil {
+		return 0, err
 	}
 
 	ec := mapMethod.Call(nil)


Test patch:
diff --git a/util/reflect_exported_test.go b/util/reflect_exported_test.go
index b7fdd6c..7bd0b26 100644
--- a/util/reflect_exported_test.go
+++ b/util/reflect_exported_test.go
@@ -106,6 +106,53 @@ func TestIsValueNilOrDefault(t *testing.T) {
 }
 
 func TestInsertIntoMap(t *testing.T) {
+	tests := []struct {
+		desc          string
+		inMap         interface{}
+		inKey         interface{}
+		inValue       interface{}
+		wantMap       interface{}
+		wantErrSubstr string
+	}{{
+		desc:    "regular map",
+		inMap:   map[int]string{42: "forty two", 43: "forty three"},
+		inKey:   44,
+		inValue: "forty four",
+		wantMap: map[int]string{42: "forty two", 43: "forty three", 44: "forty four"},
+	}, {
+		desc:          "bad map",
+		inMap:         &struct{}{},
+		inKey:         44,
+		inValue:       "forty four",
+		wantErrSubstr: `InsertIntoMap parent type is *struct {}, must be map`,
+	}}
+
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
+			err := util.InsertIntoMap(tt.inMap, tt.inKey, tt.inValue)
+			if diff := errdiff.Substring(err, tt.wantErrSubstr); diff != "" {
+				t.Fatalf("InsertIntoMap: %s", diff)
+			}
+			if err != nil {
+				return
+			}
+
+			if diff := cmp.Diff(tt.wantMap, tt.inMap, cmp.AllowUnexported(ctestschema.OrderedList_OrderedMap{})); diff != "" {
+				t.Errorf("(-want, +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+// goOrderedList is a convenience interface for ygot.GoOrderedList. It is here
+// to avoid a circular dependency.
+type goOrderedList interface {
+	// IsYANGOrderedList is a marker method that indicates that the struct
+	// implements the goOrderedList interface.
+	IsYANGOrderedList()
+}
+
+func TestAppendIntoOrderedMap(t *testing.T) {
 	om := ctestschema.GetOrderedMap(t)
 	newKey := "new"
 	for om.Get(newKey) != nil {
@@ -121,34 +168,20 @@ func TestInsertIntoMap(t *testing.T) {
 
 	tests := []struct {
 		desc          string
-		inMap         interface{}
-		inKey         interface{}
+		inMap         goOrderedList
 		inValue       interface{}
-		wantMap       interface{}
+		wantMap       goOrderedList
 		wantErrSubstr string
 	}{{
-		desc:    "regular map",
-		inMap:   map[int]string{42: "forty two", 43: "forty three"},
-		inKey:   44,
-		inValue: "forty four",
-		wantMap: map[int]string{42: "forty two", 43: "forty three", 44: "forty four"},
-	}, {
 		desc:    "ordered map",
 		inMap:   om,
-		inKey:   newKey,
 		inValue: newOrderedListElement,
 		wantMap: om2,
-	}, {
-		desc:          "bad map",
-		inMap:         &struct{}{},
-		inKey:         44,
-		inValue:       "forty four",
-		wantErrSubstr: `InsertIntoMap parent type is *struct {}, must be map`,
 	}}
 
 	for _, tt := range tests {
 		t.Run(tt.desc, func(t *testing.T) {
-			err := util.InsertIntoMap(tt.inMap, tt.inKey, tt.inValue)
+			err := util.AppendIntoOrderedMap(tt.inMap, tt.inValue)
 			if diff := errdiff.Substring(err, tt.wantErrSubstr); diff != "" {
 				t.Fatalf("InsertIntoMap: %s", diff)
 			}
diff --git a/ytypes/list_exported_test.go b/ytypes/list_exported_test.go
new file mode 100644
index 0000000..f0d6c22
--- /dev/null
+++ b/ytypes/list_exported_test.go
@@ -0,0 +1,349 @@
+// Copyright 2023 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ytypes_test
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/h-fam/errdiff"
+	"github.com/openconfig/goyang/pkg/yang"
+	"github.com/openconfig/ygot/integration_tests/schemaops/ctestschema"
+	"github.com/openconfig/ygot/ygot"
+	"github.com/openconfig/ygot/ytypes"
+)
+
+// addParents adds parent pointers for a schema tree.
+func addParents(e *yang.Entry) {
+	for _, c := range e.Dir {
+		c.Parent = e
+		addParents(c)
+	}
+}
+
+func TestUnmarshalKeyedList(t *testing.T) {
+	keyListSchema := func() *yang.Entry {
+		return &yang.Entry{
+			Name:     "key-list",
+			Kind:     yang.DirectoryEntry,
+			ListAttr: yang.NewDefaultListAttr(),
+			Key:      "key",
+			Config:   yang.TSTrue,
+			Dir: map[string]*yang.Entry{
+				"key": {
+					Kind: yang.LeafEntry,
+					Name: "key",
+					Type: &yang.YangType{Kind: yang.Ystring},
+				},
+				"leaf-field": {
+					Kind: yang.LeafEntry,
+					Name: "leaf-field",
+					Type: &yang.YangType{Kind: yang.Yint32},
+				},
+				"leaf-field2": {
+					Kind: yang.LeafEntry,
+					Name: "leaf-field2",
+					Type: &yang.YangType{Kind: yang.Yint32},
+				},
+			},
+		}
+	}
+
+	containerWithLeafListSchema := &yang.Entry{
+		Name: "container",
+		Kind: yang.DirectoryEntry,
+		Dir: map[string]*yang.Entry{
+			"key-list": keyListSchema(),
+		},
+	}
+	addParents(containerWithLeafListSchema)
+
+	containerWithPreferConfigSchema := &yang.Entry{
+		Name: "container",
+		Kind: yang.DirectoryEntry,
+		Dir: map[string]*yang.Entry{
+			"config": {
+				Name: "config",
+				Kind: yang.DirectoryEntry,
+				Dir: map[string]*yang.Entry{
+					"key-list": keyListSchema(),
+				},
+			},
+			"state": {
+				Name: "state",
+				Kind: yang.DirectoryEntry,
+				Dir: map[string]*yang.Entry{
+					"key-list": keyListSchema(),
+				},
+			},
+		},
+	}
+	addParents(containerWithPreferConfigSchema)
+
+	type ListElemStruct struct {
+		Key        *string `path:"key"`
+		LeafField  *int32  `path:"leaf-field"`
+		LeafField2 *int32  `path:"leaf-field2"`
+	}
+	type ContainerStruct struct {
+		KeyList map[string]*ListElemStruct `path:"key-list"`
+	}
+
+	type ContainerStructPreferConfig struct {
+		KeyList map[string]*ListElemStruct `path:"config/key-list" shadow-path:"state/key-list"`
+	}
+
+	tests := []struct {
+		desc    string
+		json    string
+		schema  *yang.Entry
+		parent  interface{}
+		want    interface{}
+		opts    []ytypes.UnmarshalOpt
+		wantErr string
+	}{
+		{
+			desc:   "success",
+			json:   `{ "key-list" : [ { "key" : "forty-two", "leaf-field" : 42} ] }`,
+			schema: containerWithLeafListSchema,
+			parent: &ContainerStruct{},
+			want: &ContainerStruct{
+				KeyList: map[string]*ListElemStruct{
+					"forty-two": {
+						Key:       ygot.String("forty-two"),
+						LeafField: ygot.Int32(42),
+					},
+				},
+			},
+		},
+		{
+			desc:   "success with config path",
+			json:   `{ "config": { "key-list" : [ { "key" : "forty-two", "leaf-field" : 42} ] } }`,
+			schema: containerWithPreferConfigSchema,
+			parent: &ContainerStructPreferConfig{},
+			want: &ContainerStructPreferConfig{
+				KeyList: map[string]*ListElemStruct{
+					"forty-two": {
+						Key:       ygot.String("forty-two"),
+						LeafField: ygot.Int32(42),
+					},
+				},
+			},
+		},
+		{
+			desc:   "success with already-instantiated list element",
+			json:   `{ "key-list" : [ { "key" : "forty-two", "leaf-field" : 42} ] }`,
+			schema: containerWithLeafListSchema,
+			parent: &ContainerStruct{
+				KeyList: map[string]*ListElemStruct{
+					"forty-two": {
+						Key:        ygot.String("forty-two"),
+						LeafField2: ygot.Int32(43),
+					},
+				},
+			},
+			want: &ContainerStruct{
+				KeyList: map[string]*ListElemStruct{
+					"forty-two": {
+						Key:        ygot.String("forty-two"),
+						LeafField:  ygot.Int32(42),
+						LeafField2: ygot.Int32(43),
+					},
+				},
+			},
+		},
+		{
+			desc:   "success ignoring shadowed state path",
+			json:   `{ "state": { "key-list" : [ { "key" : "forty-two", "leaf-field" : 42} ] } }`,
+			schema: containerWithPreferConfigSchema,
+			parent: &ContainerStructPreferConfig{},
+			want:   &ContainerStructPreferConfig{},
+		},
+		{
+			desc:   "success ignoring path with preferShadowPath",
+			json:   `{ "config": { "key-list" : [ { "key" : "forty-two", "leaf-field" : 42} ] } }`,
+			opts:   []ytypes.UnmarshalOpt{&ytypes.PreferShadowPath{}},
+			schema: containerWithPreferConfigSchema,
+			parent: &ContainerStructPreferConfig{},
+			want:   &ContainerStructPreferConfig{},
+		},
+		{
+			desc:   "success unmarshalling shadow path",
+			json:   `{ "state": { "key-list" : [ { "key" : "forty-two", "leaf-field" : 42} ] } }`,
+			opts:   []ytypes.UnmarshalOpt{&ytypes.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} ] }`,
+			schema:  containerWithLeafListSchema,
+			parent:  &ContainerStruct{},
+			wantErr: `parent container key-list (type *ytypes_test.ListElemStruct): JSON contains unexpected field bad-field`,
+		},
+		{
+			desc:   "ignore unknown field",
+			json:   `{ "key-list" : [ { "key" : "forty-two", "bad-field" : 42} ] }`,
+			opts:   []ytypes.UnmarshalOpt{&ytypes.IgnoreExtraFields{}},
+			schema: containerWithLeafListSchema,
+			parent: &ContainerStruct{},
+			want: &ContainerStruct{
+				KeyList: map[string]*ListElemStruct{
+					"forty-two": {
+						Key: ygot.String("forty-two"),
+					},
+				},
+			},
+		},
+		{
+			desc:   "success with ordered map",
+			json:   `{ "ordered-lists": { "ordered-list" : [ { "key" : "foo", "config": { "value" : "foo-val" } }, { "key" : "bar", "config": { "value" : "bar-val" } } ] } }`,
+			schema: ctestschema.SchemaTree["Device"],
+			parent: &ctestschema.Device{},
+			want: &ctestschema.Device{
+				OrderedList: ctestschema.GetOrderedMap(t),
+			},
+		},
+		{
+			desc:   "success with nested ordered map",
+			json:   `{ "ordered-lists": { "ordered-list" : [ { "key" : "foo", "config": { "value" : "foo-val" }, "ordered-lists": { "ordered-list" : [ { "key" : "foo", "config": { "value" : "foo-val" } }, { "key" : "bar", "config": { "value" : "bar-val" } } ] } }, { "key" : "bar", "config": { "value" : "bar-val" } } ] } }`,
+			schema: ctestschema.SchemaTree["Device"],
+			parent: &ctestschema.Device{},
+			want: &ctestschema.Device{
+				OrderedList: ctestschema.GetNestedOrderedMap(t),
+			},
+		},
+	}
+
+	var jsonTree interface{}
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
+			if err := json.Unmarshal([]byte(tt.json), &jsonTree); err != nil {
+				t.Fatalf("%s : %s", tt.desc, err)
+			}
+
+			err := ytypes.Unmarshal(tt.schema, tt.parent, jsonTree, tt.opts...)
+			if diff := errdiff.Text(err, tt.wantErr); diff != "" {
+				t.Fatalf("%s: Unmarshal error not expected:\n%s", tt.desc, diff)
+			}
+			if err == nil {
+				got, want := tt.parent, tt.want
+				if diff := cmp.Diff(want, got, cmp.AllowUnexported(ctestschema.OrderedList_OrderedMap{}, ctestschema.OrderedList_OrderedList_OrderedMap{})); diff != "" {
+					t.Errorf("%s: Unmarshal (-want, +got):\n%s", tt.desc, diff)
+				}
+			}
+		})
+	}
+}
+
+func TestUnmarshalSingleListElement(t *testing.T) {
+	listSchema := &yang.Entry{
+		Name:     "struct-list",
+		Kind:     yang.DirectoryEntry,
+		ListAttr: yang.NewDefaultListAttr(),
+		Dir: map[string]*yang.Entry{
+			"leaf-field": {
+				Kind: yang.LeafEntry,
+				Name: "leaf-field",
+				Type: &yang.YangType{Kind: yang.Yint32},
+			},
+			"enum-leaf-field": {
+				Kind: yang.LeafEntry,
+				Name: "enum-leaf-field",
+				Type: &yang.YangType{Kind: yang.Yenum},
+			},
+			"leaf2-field": {
+				Kind: yang.LeafEntry,
+				Name: "leaf2-field",
+				Type: &yang.YangType{Kind: yang.Yint64},
+			},
+		},
+	}
+
+	type ListElemStruct struct {
+		LeafName  *int32          `path:"leaf-field"`
+		EnumLeaf  ytypes.EnumType `path:"enum-leaf-field"`
+		Leaf2Name *int64          `path:"leaf2-field"`
+	}
+
+	tests := []struct {
+		desc    string
+		schema  *yang.Entry
+		json    string
+		parent  any
+		want    any
+		wantErr string
+	}{
+		{
+			desc:   "success",
+			schema: listSchema,
+			json:   `{ "leaf-field" : 42, "enum-leaf-field" : "E_VALUE_FORTY_TWO"}`,
+			parent: &ListElemStruct{
+				Leaf2Name: ygot.Int64(42),
+			},
+			want: &ListElemStruct{
+				LeafName:  ygot.Int32(42),
+				Leaf2Name: ygot.Int64(42),
+				EnumLeaf:  42,
+			},
+		},
+		{
+			desc:    "bad field",
+			schema:  listSchema,
+			json:    `{ "leaf-field" : 42, "bad-field" : "E_VALUE_FORTY_TWO"}`,
+			parent:  &ListElemStruct{},
+			wantErr: `parent container struct-list (type *ytypes_test.ListElemStruct): JSON contains unexpected field bad-field`,
+		},
+		{
+			desc:   "success with ordered map -- this should be the same as a regular map object",
+			json:   `{ "key" : "foo", "config": { "value" : "foo-val"} }`,
+			schema: ctestschema.SchemaTree["OrderedList"],
+			parent: &ctestschema.OrderedList{},
+			want: &ctestschema.OrderedList{
+				Key:   ygot.String("foo"),
+				Value: ygot.String("foo-val"),
+			},
+		},
+	}
+
+	var jsonTree interface{}
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
+			if err := json.Unmarshal([]byte(tt.json), &jsonTree); err != nil {
+				t.Fatalf("%s : %s", tt.desc, err)
+			}
+
+			err := ytypes.Unmarshal(tt.schema, tt.parent, jsonTree)
+			if diff := errdiff.Text(err, tt.wantErr); diff != "" {
+				t.Fatalf("%s: Unmarshal error not expected:\n%s", tt.desc, diff)
+			}
+			if err == nil {
+				got, want := tt.parent, tt.want
+				if diff := cmp.Diff(want, got); diff != "" {
+					t.Errorf("%s: Unmarshal (-want, +got):\n%s", tt.desc, diff)
+				}
+			}
+		})
+	}
+}
diff --git a/ytypes/list_test.go b/ytypes/list_test.go
index f88d30e..5addec3 100644
--- a/ytypes/list_test.go
+++ b/ytypes/list_test.go
@@ -524,212 +524,6 @@ func TestUnmarshalUnkeyedList(t *testing.T) {
 	}
 }
 
-func TestUnmarshalKeyedList(t *testing.T) {
-	keyListSchema := func() *yang.Entry {
-		return &yang.Entry{
-			Name:     "key-list",
-			Kind:     yang.DirectoryEntry,
-			ListAttr: yang.NewDefaultListAttr(),
-			Key:      "key",
-			Config:   yang.TSTrue,
-			Dir: map[string]*yang.Entry{
-				"key": {
-					Kind: yang.LeafEntry,
-					Name: "key",
-					Type: &yang.YangType{Kind: yang.Ystring},
-				},
-				"leaf-field": {
-					Kind: yang.LeafEntry,
-					Name: "leaf-field",
-					Type: &yang.YangType{Kind: yang.Yint32},
-				},
-				"leaf-field2": {
-					Kind: yang.LeafEntry,
-					Name: "leaf-field2",
-					Type: &yang.YangType{Kind: yang.Yint32},
-				},
-			},
-		}
-	}
-
-	containerWithLeafListSchema := &yang.Entry{
-		Name: "container",
-		Kind: yang.DirectoryEntry,
-		Dir: map[string]*yang.Entry{
-			"key-list": keyListSchema(),
-		},
-	}
-	addParents(containerWithLeafListSchema)
-
-	containerWithPreferConfigSchema := &yang.Entry{
-		Name: "container",
-		Kind: yang.DirectoryEntry,
-		Dir: map[string]*yang.Entry{
-			"config": {
-				Name: "config",
-				Kind: yang.DirectoryEntry,
-				Dir: map[string]*yang.Entry{
-					"key-list": keyListSchema(),
-				},
-			},
-			"state": {
-				Name: "state",
-				Kind: yang.DirectoryEntry,
-				Dir: map[string]*yang.Entry{
-					"key-list": keyListSchema(),
-				},
-			},
-		},
-	}
-	addParents(containerWithPreferConfigSchema)
-
-	type ListElemStruct struct {
-		Key        *string `path:"key"`
-		LeafField  *int32  `path:"leaf-field"`
-		LeafField2 *int32  `path:"leaf-field2"`
-	}
-	type ContainerStruct struct {
-		KeyList map[string]*ListElemStruct `path:"key-list"`
-	}
-
-	type ContainerStructPreferConfig struct {
-		KeyList map[string]*ListElemStruct `path:"config/key-list" shadow-path:"state/key-list"`
-	}
-
-	tests := []struct {
-		desc    string
-		json    string
-		schema  *yang.Entry
-		parent  interface{}
-		want    interface{}
-		opts    []UnmarshalOpt
-		wantErr string
-	}{
-		{
-			desc:   "success",
-			json:   `{ "key-list" : [ { "key" : "forty-two", "leaf-field" : 42} ] }`,
-			schema: containerWithLeafListSchema,
-			parent: &ContainerStruct{},
-			want: &ContainerStruct{
-				KeyList: map[string]*ListElemStruct{
-					"forty-two": {
-						Key:       ygot.String("forty-two"),
-						LeafField: ygot.Int32(42),
-					},
-				},
-			},
-		},
-		{
-			desc:   "success with already-instantiated list element",
-			json:   `{ "key-list" : [ { "key" : "forty-two", "leaf-field" : 42} ] }`,
-			schema: containerWithLeafListSchema,
-			parent: &ContainerStruct{
-				KeyList: map[string]*ListElemStruct{
-					"forty-two": {
-						Key:        ygot.String("forty-two"),
-						LeafField2: ygot.Int32(43),
-					},
-				},
-			},
-			want: &ContainerStruct{
-				KeyList: map[string]*ListElemStruct{
-					"forty-two": {
-						Key:        ygot.String("forty-two"),
-						LeafField:  ygot.Int32(42),
-						LeafField2: ygot.Int32(43),
-					},
-				},
-			},
-		},
-		{
-			desc:   "success with config path",
-			json:   `{ "config": { "key-list" : [ { "key" : "forty-two", "leaf-field" : 42} ] } }`,
-			schema: containerWithPreferConfigSchema,
-			parent: &ContainerStructPreferConfig{},
-			want: &ContainerStructPreferConfig{
-				KeyList: map[string]*ListElemStruct{
-					"forty-two": {
-						Key:       ygot.String("forty-two"),
-						LeafField: ygot.Int32(42),
-					},
-				},
-			},
-		},
-		{
-			desc:   "success ignoring shadowed state path",
-			json:   `{ "state": { "key-list" : [ { "key" : "forty-two", "leaf-field" : 42} ] } }`,
-			schema: containerWithPreferConfigSchema,
-			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} ] }`,
-			schema:  containerWithLeafListSchema,
-			parent:  &ContainerStruct{},
-			wantErr: `parent container key-list (type *ytypes.ListElemStruct): JSON contains unexpected field bad-field`,
-		},
-		{
-			desc:   "ignore unknown field",
-			json:   `{ "key-list" : [ { "key" : "forty-two", "bad-field" : 42} ] }`,
-			opts:   []UnmarshalOpt{&IgnoreExtraFields{}},
-			schema: containerWithLeafListSchema,
-			parent: &ContainerStruct{},
-			want: &ContainerStruct{
-				KeyList: map[string]*ListElemStruct{
-					"forty-two": {
-						Key: ygot.String("forty-two"),
-					},
-				},
-			},
-		},
-	}
-
-	var jsonTree interface{}
-	for _, tt := range tests {
-		t.Run(tt.desc, func(t *testing.T) {
-			if err := json.Unmarshal([]byte(tt.json), &jsonTree); err != nil {
-				t.Fatalf("%s : %s", tt.desc, err)
-			}
-
-			err := Unmarshal(tt.schema, tt.parent, 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)
-			}
-			testErrLog(t, tt.desc, err)
-			if err == nil {
-				got, want := tt.parent, tt.want
-				if diff := cmp.Diff(want, got); diff != "" {
-					t.Errorf("%s: Unmarshal (-want, +got):\n%s", tt.desc, diff)
-				}
-			}
-		})
-	}
-}
-
 func TestUnmarshalStructKeyedList(t *testing.T) {
 	containerWithLeafListSchema := &yang.Entry{
 		Name: "container",
@@ -828,84 +622,6 @@ func TestUnmarshalStructKeyedList(t *testing.T) {
 	}
 }
 
-func TestUnmarshalSingleListElement(t *testing.T) {
-	listSchema := &yang.Entry{
-		Name:     "struct-list",
-		Kind:     yang.DirectoryEntry,
-		ListAttr: yang.NewDefaultListAttr(),
-		Dir: map[string]*yang.Entry{
-			"leaf-field": {
-				Kind: yang.LeafEntry,
-				Name: "leaf-field",
-				Type: &yang.YangType{Kind: yang.Yint32},
-			},
-			"enum-leaf-field": {
-				Kind: yang.LeafEntry,
-				Name: "enum-leaf-field",
-				Type: &yang.YangType{Kind: yang.Yenum},
-			},
-			"leaf2-field": {
-				Kind: yang.LeafEntry,
-				Name: "leaf2-field",
-				Type: &yang.YangType{Kind: yang.Yint64},
-			},
-		},
-	}
-
-	type ListElemStruct struct {
-		LeafName  *int32   `path:"leaf-field"`
-		EnumLeaf  EnumType `path:"enum-leaf-field"`
-		Leaf2Name *int64   `path:"leaf2-field"`
-	}
-
-	tests := []struct {
-		desc     string
-		json     string
-		inParent any
-		want     any
-		wantErr  string
-	}{
-		{
-			desc: "success",
-			inParent: &ListElemStruct{
-				Leaf2Name: ygot.Int64(42),
-			},
-			json: `{ "leaf-field" : 42, "enum-leaf-field" : "E_VALUE_FORTY_TWO"}`,
-			want: &ListElemStruct{
-				LeafName:  ygot.Int32(42),
-				Leaf2Name: ygot.Int64(42),
-				EnumLeaf:  42,
-			},
-		},
-		{
-			desc:     "bad field",
-			inParent: &ListElemStruct{},
-			json:     `{ "leaf-field" : 42, "bad-field" : "E_VALUE_FORTY_TWO"}`,
-			wantErr:  `parent container struct-list (type *ytypes.ListElemStruct): JSON contains unexpected field bad-field`,
-		},
-	}
-
-	var jsonTree interface{}
-	for _, tt := range tests {
-		t.Run(tt.desc, func(t *testing.T) {
-			if err := json.Unmarshal([]byte(tt.json), &jsonTree); err != nil {
-				t.Fatalf("%s : %s", tt.desc, err)
-			}
-
-			err := Unmarshal(listSchema, tt.inParent, jsonTree)
-			if got, want := errToString(err), tt.wantErr; got != want {
-				t.Errorf("%s: Unmarshal got error: %v, want error: %v", tt.desc, got, want)
-			}
-			testErrLog(t, tt.desc, err)
-			if err == nil {
-				if diff := cmp.Diff(tt.inParent, tt.want); diff != "" {
-					t.Errorf("%s: Unmarshal (-want, +got):\n%s", tt.desc, diff)
-				}
-			}
-		})
-	}
-}
-
 type KeyStructMapCreation struct {
 	Key1           string              `path:"key1"`
 	Key2           int32               `path:"key2"`