Master dyno: Simplify Go JSON/YAML Handling with Dynamic Maps
This article explains how the Go dyno package lets you effortlessly read, modify, and serialize deeply‑nested JSON/YAML structures by providing Get, Set, Delete, Append and conversion utilities that work with map[string]interface{}, map[interface{}]interface{} and []interface{} without using reflection.
Introduction to dyno package
The dyno package is designed to simplify operations on dynamic objects. When serializing or deserializing JSON or YAML in Go, developers usually represent objects with map[string]interface{} or map[interface{}]interface{} and arrays with []interface{}. dyno supports CRUD operations on these mixed‑type structures at any nesting depth.
dyno package overview
dyno implements a set of functions that can navigate and manipulate arbitrary nested maps and slices. The core functions include Get, Set, Delete, Append, and conversion helpers such as ConvertMapI2MapS. All functions work without reflection, offering good performance.
Get function
The Get function retrieves a value from a dynamic structure (e.g., map[string]interface{} or []interface{}) by following a path of keys and/or indexes. If the path is empty, the original value is returned. The implementation iterates over the path and, depending on the current node type, extracts the next element:
If the node is map[string]interface{}, the path element must be a string key.
If the node is map[interface{}]interface{}, the path element can be any type and is used directly as the key.
If the node is []interface{}, the path element must be an int index.
Any other type results in an error.
After processing all elements, the final value is returned.
func Get(v interface{}, path ...interface{}) (interface{}, error) {
for i, el := range path {
switch node := v.(type) {
case map[string]interface{}:
key, ok := el.(string)
if !ok {
return nil, fmt.Errorf("expected string path element, got: %T (path element idx: %d)", el, i)
}
v, ok = node[key]
if !ok {
return nil, fmt.Errorf("missing key: %s (path element idx: %d)", key, i)
}
case map[interface{}]interface{}:
var ok bool
v, ok = node[el]
if !ok {
return nil, fmt.Errorf("missing key: %v (path element idx: %d)", el, i)
}
case []interface{}:
idx, ok := el.(int)
if !ok {
return nil, fmt.Errorf("expected int path element, got: %T (path element idx: %d)", el, i)
}
if idx < 0 || idx >= len(node) {
return nil, fmt.Errorf("index out of range: %d (path element idx: %d)", idx, i)
}
v = node[idx]
default:
return nil, fmt.Errorf("expected map or slice node, got: %T (path element idx: %d)", node, i)
}
}
return v, nil
}Typed getters (GetInt, GetInteger, etc.)
Typed wrappers such as GetInt and GetInteger call Get to obtain a value and then perform a type assertion or conversion. For example, GetInt returns an int and reports an error if the underlying value is not an int. GetInteger accepts many numeric types and strings, converting them to int64.
// GetInt returns an int value denoted by the path.
func GetInt(v interface{}, path ...interface{}) (int, error) {
v, err := Get(v, path...)
if err != nil {
return 0, err
}
i, ok := v.(int)
if !ok {
return 0, fmt.Errorf("expected int value, got: %T", v)
}
return i, nil
} // GetInteger returns an int64 value denoted by the path.
func GetInteger(v interface{}, path ...interface{}) (int64, error) {
v, err := Get(v, path...)
if err != nil {
return 0, err
}
switch i := v.(type) {
case int64:
return i, nil
case int:
return int64(i), nil
case int32:
return int64(i), nil
case int16:
return int64(i), nil
case int8:
return int64(i), nil
case uint:
return int64(i), nil
case uint64:
return int64(i), nil
case uint32:
return int64(i), nil
case uint16:
return int64(i), nil
case uint8:
return int64(i), nil
case float64:
return int64(i), nil
case float32:
return int64(i), nil
case string:
var n int64
_, err := fmt.Sscan(i, &n)
return n, err
case interface{ Int64() (int64, error) }:
return i.Int64()
default:
return 0, fmt.Errorf("expected some form of integer number, got: %T", v)
}
}SGet – string‑only path getter
SGetworks on map[string]interface{} structures and accepts only string keys in the path. It returns the value at the specified location or an error if a key is missing or a non‑map node is encountered.
func SGet(m map[string]interface{}, path ...string) (interface{}, error) {
if len(path) == 0 {
return m, nil
}
lastIdx := len(path) - 1
var value interface{}
var ok bool
for i, key := range path {
if value, ok = m[key]; !ok {
return nil, fmt.Errorf("missing key: %s (path element idx: %d)", key, i)
}
if i == lastIdx {
break
}
m2, ok := value.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("expected map with string keys node, got: %T (path element idx: %d)", value, i)
}
m = m2
}
return value, nil
}Set – modify a value
Setupdates a value at a given path inside a dynamic structure. It first retrieves the parent node (using Get on the path without the last element) and then assigns the new value based on whether the parent is a map or slice.
func Set(v interface{}, value interface{}, path ...interface{}) error {
if len(path) == 0 {
return fmt.Errorf("path cannot be empty")
}
i := len(path) - 1
if len(path) > 1 {
var err error
v, err = Get(v, path[:i]...)
if err != nil {
return err
}
}
el := path[i]
switch node := v.(type) {
case map[string]interface{}:
key, ok := el.(string)
if !ok {
return fmt.Errorf("expected string path element, got: %T (path element idx: %d)", el, i)
}
node[key] = value
case map[interface{}]interface{}:
node[el] = value
case []interface{}:
idx, ok := el.(int)
if !ok {
return fmt.Errorf("expected int path element, got: %T (path element idx: %d)", el, i)
}
if idx < 0 || idx >= len(node) {
return fmt.Errorf("index out of range: %d (path element idx: %d)", idx, i)
}
node[idx] = value
default:
return fmt.Errorf("expected map or slice node, got: %T (path element idx: %d)", node, i)
}
return nil
}Append – add an element to a slice
Appendlocates a slice via a path, appends a new element, and writes the updated slice back using Set. It validates that the target node is indeed a slice.
func Append(v interface{}, value interface{}, path ...interface{}) error {
if len(path) == 0 {
return fmt.Errorf("path cannot be empty")
}
node, err := Get(v, path...)
if err != nil {
return err
}
s, ok := node.([]interface{})
if !ok {
return fmt.Errorf("expected slice node, got: %T (path element idx: %d)", node, len(path)-1)
}
return Set(v, append(s, value), path...)
}Delete – remove a key or element
Deleteremoves a key from a map or an element from a slice. For slices it shifts elements left, clears the last slot to avoid memory leaks, and writes the shortened slice back.
func Delete(v interface{}, key interface{}, path ...interface{}) error {
if len(path) == 0 {
if _, ok := v.([]interface{}); ok {
return fmt.Errorf("path cannot be empty if v is a slice")
}
}
node, err := Get(v, path...)
if err != nil {
return err
}
switch node2 := node.(type) {
case map[string]interface{}:
skey, ok := key.(string)
if !ok {
return fmt.Errorf("expected string key, got: %T", key)
}
delete(node2, skey)
case map[interface{}]interface{}:
delete(node2, key)
case []interface{}:
idx, ok := key.(int)
if !ok {
return fmt.Errorf("expected int key, got: %T", key)
}
if idx < 0 || idx >= len(node2) {
return fmt.Errorf("index out of range: %d", idx)
}
copy(node2[idx:], node2[idx+1:])
node2[len(node2)-1] = nil
return Set(v, node2[:len(node2)-1], path...)
default:
return fmt.Errorf("expected map or slice node, got: %T (path element idx: %d)", node, len(path)-1)
}
return nil
}ConvertMapI2MapS – map key conversion
The ConvertMapI2MapS function recursively walks a dynamic object and converts any map[interface{}]interface{} into map[string]interface{}. Keys that are not strings are turned into strings using fmt.Sprint. This is useful before JSON marshaling, which does not accept non‑string map keys.
func ConvertMapI2MapS(v interface{}) interface{} {
switch x := v.(type) {
case map[interface{}]interface{}:
m := map[string]interface{}{}
for k, v2 := range x {
switch k2 := k.(type) {
case string:
m[k2] = ConvertMapI2MapS(v2)
default:
m[fmt.Sprint(k)] = ConvertMapI2MapS(v2)
}
}
v = m
case []interface{}:
for i, v2 := range x {
x[i] = ConvertMapI2MapS(v2)
}
case map[string]interface{}:
for k, v2 := range x {
x[k] = ConvertMapI2MapS(v2)
}
}
return v
}Typical usage scenario
dyno is ideal for processing deeply nested YAML/JSON data without defining static structs. You can read a value with dyno.Get, modify it using dyno.Set or dyno.Append, and finally marshal the structure to JSON after converting map keys with dyno.ConvertMapI2MapS. Because dyno avoids reflection, it offers good performance.
Conclusion
The article demonstrated how the dyno package enables convenient handling of dynamic data in Go, covering getters, setters, deletion, appending, and map‑key conversion. Its reflection‑free implementation makes it a performant choice for JSON/YAML manipulation.
Further reading
dyno package documentation: https://pkg.go.dev/github.com/icza/dyno
dyno GitHub repository: https://github.com/icza/dyno
Go YAML‑to‑JSON pitfalls (original Chinese article)
Example code repository: https://github.com/jianghushinian/blog-go-example/tree/main/yaml/dyno
Permanent article URL: https://jianghushinian.cn/2025/08/11/go-dyno/
Go Programming World
Mobile version of tech blog https://jianghushinian.cn/, covering Golang, Docker, Kubernetes and beyond.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
