Recursive AI

yaml.v3 is archived: what to use instead

gopkg.in/yaml.v3 is archived and unmaintained. Four ways off it, from a one-line import change to a YAML 1.2 API, each with code run on the current release.

If you need a yaml.v3 alternative because gopkg.in/yaml.v3 is archived, the smallest move is an import-path change to go.yaml.in/yaml/v3, the same code under the YAML organization with security fixes. The other three routes are its successor go.yaml.in/yaml/v4, still a release candidate on 26 October 2026; a different library, such as goccy/go-yaml; or oneyaml, whose compat/yamlv3 package keeps your yaml.v3 calls compiling on a single import change, after which you can move to its YAML 1.2 API one file at a time.

What happened to gopkg.in/yaml.v3

The go-yaml/yaml repository is archived (read-only) on GitHub, and the README on its v3 branch opens with “THIS PROJECT IS UNMAINTAINED”. Its author, Gustavo Niemeyer, explains there that he couldn’t hand it to a dedicated team in time. The last release of gopkg.in/yaml.v3 is v3.0.1, from 27 May 2022, according to the Go module proxy.

The code still works, and nothing breaks today. What stops is fixes: any bug or security report from now on lands in the YAML organization’s fork, yaml/go-yaml, not in the module you import. That README lists v2 and v3 as “Stable, frozen API; security fixes only” and v4 as “Active development”.

Four ways off yaml.v3

RouteCode changeWhat you get
go.yaml.in/yaml/v3 v3.0.5The import pathThe same behavior, with security fixes. No new features
go.yaml.in/yaml/v4 v4.0.0-rc.6The import path; then review the v3 to v4 migration guide on formatting, errors and custom unmarshalersLoad/Dump with options and plugins, and v3 defaults through a preset. Not released yet: its README says it is “intended to be released in October 2026”
goccy/go-yaml v1.19.2Imports and some call sites; a different API for nodesIts own parser, an AST and YAML Path
oneyaml.dev/go/compat/yamlv3, then oneyaml.dev/go v1.0.0The import path; later, the oneyaml API file by fileyaml.v3’s API and YAML 1.1 reading first; then YAML 1.2, edits that keep comments, patches and input limits

For the first route, the program below prints exactly the same output with "go.yaml.in/yaml/v3" in place of "gopkg.in/yaml.v3"; we ran both. Indirect dependencies may still pull in gopkg.in/yaml.v3, so run go mod why gopkg.in/yaml.v3 to see which ones.

Switch to oneyaml with one import

Here is a small yaml.v3 program. Its file uses two YAML 1.1 forms that yaml.v3 still honors in typed fields: yes for a boolean and 0755 for an octal number.

main.gogopkg.in/yaml.v3 v3.0.1
package main
 
import (
    "fmt"
    "log"
 
    "gopkg.in/yaml.v3"
)
 
type Config struct {
    Name    string   `yaml:"name"`
    Enabled bool     `yaml:"enabled"`
    Mode    int      `yaml:"mode"`
    Tags    []string `yaml:"tags"`
}
 
func main() {
    var cfg Config
    src := []byte("name: web\nenabled: yes\nmode: 0755\ntags: [a, b]\n")
    if err := yaml.Unmarshal(src, &cfg); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%+v\n", cfg)
 
    out, err := yaml.Marshal(cfg)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Print(string(out))
}
Output
{Name:web Enabled:true Mode:493 Tags:[a b]}
name: web
enabled: true
mode: 493
tags:
    - a
    - b

Install oneyaml and change the import. The package name stays yaml, so no call site changes:

TerminalGo modules
go get oneyaml.dev/[email protected]
main.gooneyaml.dev/go v1.0.0
package main
 
import (
    "fmt"
    "log"
 
    yaml "oneyaml.dev/go/compat/yamlv3" // was: "gopkg.in/yaml.v3"
)
 
type Config struct {
    Name    string   `yaml:"name"`
    Enabled bool     `yaml:"enabled"`
    Mode    int      `yaml:"mode"`
    Tags    []string `yaml:"tags"`
}
 
func main() {
    var cfg Config
    src := []byte("name: web\nenabled: yes\nmode: 0755\ntags: [a, b]\n")
    if err := yaml.Unmarshal(src, &cfg); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%+v\n", cfg)
 
    out, err := yaml.Marshal(cfg)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Print(string(out))
}
Output
{Name:web Enabled:true Mode:493 Tags:[a b]}
name: web
enabled: true
mode: 493
tags:
    - a
    - b

The output is byte for byte the same. The adapter covers Unmarshal, Marshal, Decoder, Encoder and yaml.Node with its comments, and reads YAML 1.1 forms the way yaml.v3 does, so your existing files keep their meaning (switch from yaml.v3 with one import).

What to check after the swap

  • Numbers: a plain 3.5 read into an integer field is a type error instead of being truncated to 3.
  • Invalid and unusual input: valid YAML that yaml.v3 refused is read, invalid YAML that yaml.v3 accepted is refused, and input that made yaml.v3 panic returns an error. Tests that pin yaml.v3’s behavior on such input will change.
  • Custom unmarshalers: an UnmarshalYAML that takes gopkg.in/yaml.v3’s own *yaml.Node, in a type from another module, can’t be called by the adapter. The migration guide shows how to wrap it.
  • Limits: reading runs under oneyaml’s default limits on depth and alias expansion, which yaml.v3 doesn’t have, so very deep or alias-heavy input can now return an error.

Move to the oneyaml API

The adapter is a bridge. The oneyaml package itself reads YAML 1.2, so the same file means something different: yes is a string, and 0755 is the decimal 755. Here is the same program on the native API:

main.gooneyaml.dev/go v1.0.0
package main
 
import (
    "errors"
    "fmt"
 
    oneyaml "oneyaml.dev/go"
)
 
type Config struct {
    Name    string   `yaml:"name"`
    Enabled bool     `yaml:"enabled"`
    Mode    int      `yaml:"mode"`
    Tags    []string `yaml:"tags"`
}
 
func main() {
    var cfg Config
    src := []byte("name: web\nenabled: yes\nmode: 0755\ntags: [a, b]\n")
    err := oneyaml.Unmarshal(src, &cfg)
 
    var te *oneyaml.TypeError
    if errors.As(err, &te) {
        fmt.Println(err)
    }
    fmt.Printf("%+v\n", cfg)
}
Output
oneyaml: line 2, column 10: /enabled: cannot unmarshal !!str `yes` into bool
{Name:web Enabled:false Mode:755 Tags:[a b]}

The yes is reported with its line, column and pointer instead of becoming true, and the rest of the document is still read. That is the point of moving file by file: each file you move is one whose meaning you have checked. Fix the file (enabled: true, mode: 0o755), or keep that file on the adapter until you can.

Most calls keep their names:

yaml.v3oneyaml
yaml.Unmarshal(data, &v)oneyaml.Unmarshal(data, &v)
yaml.Marshal(v)oneyaml.Marshal(v)
yaml.NewDecoder(r), dec.Decode(&v)oneyaml.NewDecoder(r), the same
dec.KnownFields(true)oneyaml.KnownFields() passed to the call
yaml.NewEncoder(w), enc.SetIndent(n)oneyaml.NewEncoder(w, oneyaml.Indent(n))
Editing through yaml.Nodeoneyaml.Parse, then doc.Set, Remove, Rename

The full table, with errors, custom marshalers and streams, is in migrating from yaml.v3.

Why move further than go.yaml.in/yaml/v3

If yaml.v3 does everything you need, the import change is enough. These are the reasons to go further:

  • Edits that keep the file. yaml.v3’s own documentation says of Node that “the content when re-encoded will not have its original textual representation preserved” (Node). oneyaml’s Document rewrites only the values you set. Edit YAML without losing comments shows both on the same file.
  • YAML 1.2 by default, checked against the yaml-test-suite with the known deviations listed (conformance).
  • Patches. RFC 6902 JSON Patch and RFC 7396 merge patches applied to the YAML text (JSON Patch for YAML).
  • Limits on by default for depth, alias expansion and number size, with an Untrusted() preset (limits).

If you are weighing every option rather than migrating, start with choosing a Go YAML library. If you use sigs.k8s.io/yaml rather than yaml.v3, its adapter is oneyaml.dev/go/compat/k8syaml (migration guide).