---
title: "yaml.v3 is archived: what to use instead"
url: https://recursiveai.org/oneyaml/articles/yaml-v3-alternative/
description: "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."
date: 2026-10-26
author: oneyaml team
---

# 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.

October 26, 2026 · oneyaml team

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](https://github.com/go-yaml/yaml) repository is archived (read-only) on GitHub, and the README on its [v3 branch](https://github.com/go-yaml/yaml/blob/v3/README.md) 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](https://github.com/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

| Route | Code change | What you get |
|---|---|---|
| `go.yaml.in/yaml/v3` v3.0.5 | The import path | The same behavior, with security fixes. No new features |
| `go.yaml.in/yaml/v4` v4.0.0-rc.6 | The import path; then review the [v3 to v4 migration guide](https://github.com/yaml/go-yaml/blob/main/docs/v3-to-v4-migration.md) on formatting, errors and custom unmarshalers | `Load`/`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](https://github.com/goccy/go-yaml) v1.19.2 | Imports and some call sites; a different API for nodes | Its own parser, an AST and YAML Path |
| `oneyaml.dev/go/compat/yamlv3`, then `oneyaml.dev/go` v1.0.0 | The import path; later, the `oneyaml` API file by file | yaml.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.go · gopkg.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:

*Terminal · Go modules*

```
go get oneyaml.dev/go@v1.0.0
```

*main.go · oneyaml.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](https://recursiveai.org/oneyaml/go/cookbook/switch-from-yaml-v3/)).

### 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](https://recursiveai.org/oneyaml/go/docs/migrate-yaml-v3/) 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.go · oneyaml.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.v3 | oneyaml |
|---|---|
| `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.Node` | `oneyaml.Parse`, then `doc.Set`, `Remove`, `Rename` |

The full table, with errors, custom marshalers and streams, is in [migrating from yaml.v3](https://recursiveai.org/oneyaml/go/docs/migrate-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](https://pkg.go.dev/go.yaml.in/yaml/v3#Node)). oneyaml’s `Document` rewrites only the values you set. [Edit YAML without losing comments](https://recursiveai.org/oneyaml/articles/edit-yaml-keep-comments/) shows both on the same file.
- **YAML 1.2 by default**, checked against the yaml-test-suite with the known deviations listed ([conformance](https://recursiveai.org/oneyaml/go/docs/conformance/)).
- **Patches.** RFC 6902 JSON Patch and RFC 7396 merge patches applied to the YAML text ([JSON Patch for YAML](https://recursiveai.org/oneyaml/articles/json-patch-yaml/)).
- **Limits on by default** for depth, alias expansion and number size, with an `Untrusted()` preset ([limits](https://recursiveai.org/oneyaml/go/docs/limits/)).

If you are weighing every option rather than migrating, start with [choosing a Go YAML library](https://recursiveai.org/oneyaml/articles/golang-yaml/). If you use sigs.k8s.io/yaml rather than yaml.v3, its adapter is `oneyaml.dev/go/compat/k8syaml` ([migration guide](https://recursiveai.org/oneyaml/go/docs/migrate-sigs-k8s-yaml/)).
