17370845950

How to Correctly Unmarshal YAML in Go

this tutorial explains how to properly define go structs for yaml unmarshaling—especially when dealing with nested sequences (like handler lists)—and fixes the common `cannot unmarshal !!seq into map[string][]map[string][]string` error.

YAML is widely used for configuration files due to its readability, but unmarshaling it into Go structs can be tricky—particularly when the structure contains lists (-) of key-value mappings. The root cause of the error cannot unmarshal !!seq into map[string][]map[string][]string is a type mismatch: your YAML defines handlers as a sequence (i.e., a list/array) of mappings (e.g., - url: /.*), but your struct declared Handlers as map[string][]map[string][]string—a deeply nested and incompatible type.

The correct approach is to match Go types to YAML’s logical structure:

  • A YAML list (- item1, - item2) → corresponds to a Go slice: []T
  • Each list item that is a mapping (e.g., url: /.*, secure: always) → corresponds to a map[string]string, provided all keys are strings and values are scalar (strings, numbers, booleans).

Here's the corrected struct:

type AppYAML struct {
    Runtime       string              `yaml:"runtime,omitempty"`
    Handlers      []map[string]string `yaml:"handlers,omitempty"`
    Env_Variables map[string]string   `yaml:"env_variables,omitempty"`
}

✅ Why this works:

  • []map[string]string means “a slice of maps”, matching - url: ..., - static_files: ..., etc.
  • Each handler entry becomes a map[string]string, allowing access like handler

    ["url"] or handler["secure"].

⚠️ Important notes:

  • Use gopkg.in/yaml.v3 instead of v1 (which is deprecated and lacks support for newer YAML features and proper error messages).
  • For better maintainability and type safety, prefer explicit struct fields over map[string]string when the schema is known. Example:
type Handler struct {
    URL         string `yaml:"url,omitempty"`
    Runtime     string `yaml:"runtime,omitempty"`
    Secure      string `yaml:"secure,omitempty"`
    StaticFiles string `yaml:"static_files,omitempty"`
}

type AppYAML struct {
    Runtime       string   `yaml:"runtime,omitempty"`
    Handlers      []Handler `yaml:"handlers,omitempty"`
    Env_Variables map[string]string `yaml:"env_variables,omitempty"`
}

This improves readability, enables compile-time validation, and avoids runtime panics from unexpected keys.

Finally, here’s a complete working example using yaml.v3:

package main

import (
    "fmt"
    "log"
    "gopkg.in/yaml.v3"
)

type Handler struct {
    URL     string `yaml:"url,omitempty"`
    Runtime string `yaml:"runtime,omitempty"`
    Secure  string `yaml:"secure,omitempty"`
}

type AppYAML struct {
    Runtime       string            `yaml:"runtime,omitempty"`
    Handlers      []Handler         `yaml:"handlers,omitempty"`
    Env_Variables map[string]string `yaml:"env_variables,omitempty"`
}

func main() {
    s := `
runtime: go
handlers:
- url: /.*
  runtime: _go_app
  secure: always
env_variables:
  something: 'test'
`

    var a AppYAML
    if err := yaml.Unmarshal([]byte(s), &a); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Runtime: %s\n", a.Runtime)
    for i, h := range a.Handlers {
        fmt.Printf("Handler %d: url=%q, runtime=%q, secure=%q\n", 
            i+1, h.URL, h.Runtime, h.Secure)
    }
    fmt.Printf("Env: %+v\n", a.Env_Variables)
}

? Summary: Always align Go types with YAML’s hierarchical shape—[]T for sequences, map[string]T for mappings—and prefer concrete structs over generic maps when possible. Upgrade to yaml.v3, validate with real examples, and leverage struct tags for precise field control.