Recursive AI

Read YAML in Go without a struct

Read a YAML file in Go without defining a struct: unmarshal into map[string]any, read one value by path, walk unknown keys with line numbers, and write it back.

To read a YAML file in Go without a struct, unmarshal it into a map[string]any and use type assertions to reach the values. That is enough for a few top-level keys. For a value deep in the file, read it by path into a typed variable. For keys you don’t know in advance, walk the document node by node. This page shows all three with oneyaml.dev/go v1.0.0, then how to change a value and write the file back with its comments, and what yaml.v2 and yaml.v3 return for the same untyped read.

Every example reads this file:

config.yaml
# Service settings
server:
  host: 0.0.0.0
  port: 8080
  tls: true
database:
  url: postgres://db.internal:5432/app
  pool: 10
features:
  - search
  - billing

Unmarshal YAML into map[string]any

main.gooneyaml.dev/go v1.0.0
package main
 
import (
    "fmt"
    "log"
    "os"
 
    oneyaml "oneyaml.dev/go"
)
 
func main() {
    data, err := os.ReadFile("config.yaml")
    if err != nil {
        log.Fatal(err)
    }
 
    var cfg map[string]any
    if err := oneyaml.Unmarshal(data, &cfg); err != nil {
        log.Fatal(err)
    }
 
    server := cfg["server"].(map[string]any)
    port := server["port"].(int)
    fmt.Printf("port %d (%T), tls %v (%T)\n", port, port, server["tls"], server["tls"])
 
    for _, f := range cfg["features"].([]any) {
        fmt.Println("feature:", f)
    }
}
Output
port 8080 (int), tls true (bool)
feature: search
feature: billing

What each YAML node becomes inside an any, per the Unmarshal docs:

YAMLGo type
mappingmap[string]any, or map[any]any when a key isn’t a string
sequence[]any
integerint, or uint64 or *big.Int when it doesn’t fit
floatfloat64
true, falsebool
null or emptynil
anything else, timestamps includedstring

Plain scalars follow the YAML 1.2 Core schema, so yes stays the string "yes" and 0755 is the integer 755 (why that matters). The single-value assertions above panic if the file has a different shape. In real code use the two-value form, server, ok := cfg["server"].(map[string]any), at every level. That gets long quickly, which is the reason for the next section.

Read one value by path

Parse the file into a Document and read a value by its JSON Pointer into a variable of the type you want. Only the value at the end of the path needs a type, and a missing path or a wrong type is an error, not a panic:

main.gooneyaml.dev/go v1.0.0
package main
 
import (
    "fmt"
    "log"
    "os"
 
    oneyaml "oneyaml.dev/go"
)
 
func main() {
    data, err := os.ReadFile("config.yaml")
    if err != nil {
        log.Fatal(err)
    }
    doc, err := oneyaml.Parse(data)
    if err != nil {
        log.Fatal(err)
    }
 
    var port int
    if err := doc.Get("/server/port", &port); err != nil {
        log.Fatal(err)
    }
    var first string
    if err := doc.Get("/features/0", &first); err != nil {
        log.Fatal(err)
    }
    fmt.Println(port, first)
 
    var tls int
    err = doc.Get("/server/tls", &tls)
    fmt.Println(err)
    var missing int
    err = doc.Get("/server/timeout", &missing)
    fmt.Println(err)
}
Output
8080 search
oneyaml: line 5, column 8: /server/tls: cannot unmarshal !!bool `true` into int
oneyaml: get "/server/timeout": path not found

The type error names the line and column in the file, which is what you want to show a person who wrote the config. Sequence items are addressed by index (/features/0). In a pointer, / inside a key is written ~1, so build pointers from data with oneyaml.Pointer("metadata", "labels", "app.kubernetes.io/name"), which escapes each part (editing).

Walk keys you don’t know in advance

When the keys themselves are data, as in a linter, a diff tool or a generic config viewer, walk the nodes. Entries and Items are range-over-func iterators, and every node knows its pointer and position:

main.gooneyaml.dev/go v1.0.0
package main
 
import (
    "fmt"
    "log"
    "os"
 
    oneyaml "oneyaml.dev/go"
)
 
func walk(n oneyaml.Node) {
    switch n.Kind() {
    case oneyaml.MappingNode:
        for _, v := range n.Entries() {
            walk(v)
        }
    case oneyaml.SequenceNode:
        for item := range n.Items() {
            walk(item)
        }
    default:
        ptr, _ := n.Pointer()
        line, _ := n.Position()
        fmt.Printf("line %2d  %-18s %s\n", line, ptr, n.Value())
    }
}
 
func main() {
    data, err := os.ReadFile("config.yaml")
    if err != nil {
        log.Fatal(err)
    }
    doc, err := oneyaml.Parse(data)
    if err != nil {
        log.Fatal(err)
    }
    walk(doc.Root())
}
Output
line  3  /server/host       0.0.0.0
line  4  /server/port       8080
line  5  /server/tls        true
line  7  /database/url      postgres://db.internal:5432/app
line  8  /database/pool     10
line 10  /features/0        search
line 11  /features/1        billing

Value returns a scalar’s text as written. To get a typed value from a node, call node.Decode(&v). Nodes also carry their comments (HeadComment, LineComment), tag and anchor. The cookbook recipe read YAML without a struct shows those.

Change a value and write the file back

With a map, writing back means Marshal: the comment goes, and the keys come out sorted. A Document changes only the values you set:

main.gooneyaml.dev/go v1.0.0
package main
 
import (
    "log"
    "os"
 
    oneyaml "oneyaml.dev/go"
)
 
func main() {
    data, err := os.ReadFile("config.yaml")
    if err != nil {
        log.Fatal(err)
    }
    doc, err := oneyaml.Parse(data)
    if err != nil {
        log.Fatal(err)
    }
    if err := doc.Set("/server/port", 9090); err != nil {
        log.Fatal(err)
    }
    if err := doc.Set("/features/-", "audit"); err != nil {
        log.Fatal(err)
    }
    if err := os.WriteFile("config.yaml", doc.Bytes(), 0o644); err != nil {
        log.Fatal(err)
    }
}
config.yaml after the run
# Service settings
server:
  host: 0.0.0.0
  port: 9090
  tls: true
database:
  url: postgres://db.internal:5432/app
  pool: 10
features:
  - search
  - billing
  - audit

The port changed and audit was appended (/features/- means the end of the sequence). The comment, the key order and the indentation are as they were. More in edit YAML without losing comments.

yaml.v2, yaml.v3 and map[interface{}]interface{}

Older answers to this question use yaml.v2, which returns nested mappings as map[interface{}]interface{}. That breaks the assertion to map[string]any, and encoding/json can’t marshal it. This program reads the same file untyped with three libraries:

main.goGo 1.26
package main
 
import (
    "encoding/json"
    "fmt"
    "os"
 
    yamlv2 "go.yaml.in/yaml/v2"
    yamlv3 "go.yaml.in/yaml/v3"
    oneyaml "oneyaml.dev/go"
)
 
func main() {
    data, _ := os.ReadFile("config.yaml")
    decoders := []struct {
        name      string
        unmarshal func([]byte, any) error
    }{
        {"go.yaml.in/yaml/v2", yamlv2.Unmarshal},
        {"go.yaml.in/yaml/v3", yamlv3.Unmarshal},
        {"oneyaml.dev/go", func(b []byte, v any) error { return oneyaml.Unmarshal(b, v) }},
    }
    for _, d := range decoders {
        var v any
        if err := d.unmarshal(data, &v); err != nil {
            fmt.Println(d.name, "error:", err)
            continue
        }
        _, jsonErr := json.Marshal(v)
        fmt.Printf("%-19s %T\n%-19s json.Marshal error: %v\n", d.name, v, "", jsonErr)
    }
}
Output
go.yaml.in/yaml/v2  map[interface {}]interface {}
                    json.Marshal error: json: unsupported type: map[interface {}]interface {}
go.yaml.in/yaml/v3  map[string]interface {}
                    json.Marshal error: <nil>
oneyaml.dev/go      map[string]interface {}
                    json.Marshal error: <nil>

yaml.v3 (here go.yaml.in/yaml/v3 v3.0.5) returns map[string]interface{} like oneyaml, and its yaml.Node type also gives you a tree with line and column numbers (pkg.go.dev). The original gopkg.in/yaml.v3 is archived, and its successor is frozen apart from security fixes; see yaml.v3 is archived: what to use instead.

Where to go next

For a typed read when you do have a struct, see the cookbook recipe parse YAML into structs. To compare Go YAML libraries, see choosing a Go YAML library. Install and first steps: getting started. All three oneyaml libraries are on the oneyaml home page. Versions on this page are the latest published on 26 October 2026.