This guide will help you migrate your code from go.yaml.in/yaml/v3
(or gopkg.in/yaml.v3) to go.yaml.in/yaml/v4.
- Update import path
- Optionally migrate to new API (Load/Dump, Loader/Dumper)
- Adjust formatting expectations or use yaml.WithV3Defaults() preset
- Update tests
v3:
import "gopkg.in/yaml.v3"
// or
import "go.yaml.in/yaml/v3"v4:
import "go.yaml.in/yaml/v4"Update all import statements throughout your codebase.
v4 introduces a cleaner API with better naming.
v3:
err := yaml.Unmarshal(data, &config)v4:
err := yaml.Load(data, &config)v3:
data, err := yaml.Marshal(&config)v4:
data, err := yaml.Dump(&config)v3:
decoder := yaml.NewDecoder(reader)
err := decoder.Decode(&config)v4:
loader := yaml.NewLoader(reader)
err := loader.Load(&config)v3:
encoder := yaml.NewEncoder(writer)
err := encoder.Encode(&config)
encoder.Close()v4:
dumper := yaml.NewDumper(writer)
err := dumper.Dump(&config)
dumper.Close()v4 adds StreamNode, a new node kind that exposes stream-level
metadata not available in v3: encoding, %YAML version directives,
and %TAG directives.
This is accessed via Node.Stream, which is non-nil only on
StreamNode nodes.
Enable stream nodes with WithStreamNodes():
loader := yaml.NewLoader(reader, yaml.WithStreamNodes())
for {
var node yaml.Node
err := loader.Load(&node)
if errors.Is(err, io.EOF) {
break
}
if node.Kind == yaml.StreamNode && node.Stream != nil {
enc := node.Stream.Encoding
ver := node.Stream.Version // *yaml.VersionDirective
tags := node.Stream.TagDirectives // []yaml.TagDirective
}
}With stream nodes enabled, the loader emits nodes in the pattern
[Stream, Doc, Stream, Doc, ..., Stream] — one opening stream node
per document boundary, plus a final closing stream node.
v4 introduces a functional options pattern for configuration:
// Version presets
yaml.Dump(&data, yaml.WithV2Defaults()) // Use v2 defaults
yaml.Dump(&data, yaml.WithV3Defaults()) // Use v3 defaults
yaml.Dump(&data, yaml.WithV4Defaults()) // Use v4 defaults (2-space, compact)
// Custom options
yaml.Dump(&data,
yaml.WithIndent(4),
yaml.WithCompactSeqIndent(false),
yaml.WithLineWidth(100),
)
// Combine presets with overrides
yaml.Dump(&data, yaml.WithV3Defaults(), yaml.WithIndent(2))
// Loading options
yaml.Load(data, &config,
yaml.WithKnownFields(), // Strict field checking
yaml.WithUniqueKeys(), // Enforce unique keys
)Available dump options:
WithIndent(n)- Set indentation spacesWithCompactSeqIndent(bool)- Compact sequence indentationWithLineWidth(n)- Maximum line widthWithUnicode(bool)- Use Unicode charactersWithCanonical(bool)- Canonical output formatWithLineBreak(lb)- Line break style (LN, CR, CRLN)WithExplicitStart(bool)- Add---document startWithExplicitEnd(bool)- Add...document endWithFlowSimpleCollections(bool)- Use flow style for simple collections
Available load options:
WithKnownFields()- Reject unknown struct fieldsWithUniqueKeys()- Enforce unique mapping keysWithSingleDocument()- Expect only one document
You can configure options using YAML:
optsYAML := `
indent: 4
compact-seq-indent: false
line-width: 100
`
opts, err := yaml.OptsYAML(optsYAML)
if err != nil {
log.Fatal(err)
}
data, err := yaml.Dump(&config, opts)v4 has different default formatting than v3:
| Aspect | v3 Default | v4 Default |
|---|---|---|
| Indentation | 4 spaces | 2 spaces |
| Sequence style | Normal | Compact |
Example:
# v3 default output
items:
- name: foo
value: 1
- name: bar
value: 2
# v4 default output
items:
- name: foo
value: 1
- name: bar
value: 2If you need v3's formatting, use the yaml.WithV3Defaults() preset:
// Get v3-style formatting in v4
data, err := yaml.Dump(&config, yaml.WithV3Defaults())Or customize individual options:
data, err := yaml.Dump(&config,
yaml.WithIndent(4),
yaml.WithCompactSeqIndent(false),
)All v3 APIs continue to work in v4. The classic API remains supported for simple use cases:
Unmarshal()- Classic API (or useLoad()for more flexibility)Marshal()- Classic API (or useDump()for more flexibility)NewDecoder()- Classic API (or useNewLoader()for more flexibility)NewEncoder()- Classic API (or useNewDumper()for more flexibility)
You can migrate incrementally:
- Update import path to v4
- Verify tests pass
- Optionally migrate to new API for additional features
- Update TypeError.Errors handling if applicable
- Update import path
- If using TypeError.Errors directly, update that code
- Add
yaml.WithV3Defaults()preset to maintain v3 formatting - Done!
// Only change needed for basic migration
data, err := yaml.Dump(&config, yaml.WithV3Defaults())- Update import path
- Replace
Unmarshal→Load,Marshal→Dump - Replace
NewDecoder→NewLoader,NewEncoder→NewDumper - Test with v4 defaults or choose formatting explicitly
- Follow Strategy 2
- Explore new functional options
- Leverage structured error information
- Adopt version presets for different use cases
# Run your existing tests
go test ./...
# Verify YAML output formatting
# Use the go-yaml CLI tool to compare
go install go.yaml.in/yaml/v4/cmd/go-yaml@latest
./go-yaml -n < testfile.yamlSolution: Use yaml.WithV3Defaults() preset to maintain v3 formatting:
yaml.Dump(&data, yaml.WithV3Defaults())Solution: Migrate to the new API for options support:
Unmarshal→LoadMarshal→DumpNewDecoder→NewLoaderNewEncoder→NewDumper
If you encounter issues during migration:
- Check the API documentation
- Browse examples
- Open an issue
- Ask in Slack
- Explore the new functional options
- Review the examples directory
- Read the full API documentation
- Try the go-yaml CLI tool for debugging