Two ways to move from sigs.k8s.io/yaml (and its parent ghodss/yaml): keep your code and swap the import to the compat/k8syaml adapter, or move to the oneyaml API. sigs.k8s.io/yaml converts YAML to JSON and lets encoding/json bind it, so what it reads is what the JSON round trip can carry. The oneyaml package reads YAML directly.
Swap the import
# before: import "sigs.k8s.io/yaml" # after: import "oneyaml.dev/go/compat/k8syaml" go get oneyaml.dev/go@latest
The package is named yaml, as the original is, so call sites compile unchanged. It is a compatibility adapter with listed differences, not a copy. Marshal, Unmarshal, UnmarshalStrict, YAMLToJSON, YAMLToJSONStrict, JSONToYAML, JSONOpt and DisallowUnknownFields keep their names and signatures. The adapter does not provide JSONObjectToYAMLObject, which returns a type of the yaml.v2 module, or the kyaml, yamlfmt, goyaml.v2 and goyaml.v3 subpackages of sigs.k8s.io/yaml; code that uses them keeps its sigs.k8s.io/yaml import for those calls. Like sigs.k8s.io/yaml, it reads YAML 1.1 scalars (yes, on and 0755), expands merge keys, reads the first document only, and binds through the json tags of your types:
var c Container err := yaml.Unmarshal([]byte(` name: web image: nginx:1.25 args: [--port, 8080, --verbose] env: DEBUG: yes RETRIES: 3 unknown: ignored `), &c) fmt.Println(err) fmt.Printf("%s %s %q %v\n", c.Name, c.Image, c.Args, c.Env) // Output: // <nil> // web nginx:1.25 ["--port" "8080" "--verbose"] map[DEBUG:true RETRIES:3]
UnmarshalStrict and YAMLToJSONStrict also reject duplicate keys and unknown fields:
var c Container err := yaml.UnmarshalStrict([]byte("name: web\nimage: nginx\nreplicas: 3\n"), &c) fmt.Println(err != nil) // Output: // true
Options for the JSON decoder work as before; this one keeps integers above 2^53 exact:
var v map[string]any useNumber := func(d *json.Decoder) *json.Decoder { d.UseNumber(); return d } if err := yaml.Unmarshal([]byte("id: 12345678901234567890\n"), &v, useNumber); err != nil { fmt.Println(err) } fmt.Println(v["id"]) // Output: // 12345678901234567890
Marshal writes what sigs.k8s.io/yaml writes, with keys sorted:
out, err := yaml.Marshal(Container{ Name: "web", Image: "nginx:1.25", Args: []string{"--port", "8080"}, Env: map[string]string{"DEBUG": "yes", "RETRIES": "3"}, }) fmt.Println(err) fmt.Print(string(out)) // Output: // <nil> // args: // - --port // - "8080" // env: // DEBUG: "yes" // RETRIES: "3" // image: nginx:1.25 // name: web
It differs where sigs.k8s.io/yaml loses or changes data: a merge key never overrides a key written in the same mapping, a float read into a string field keeps its precision, 1 and "1" as keys are one JSON key with the last one winning, and YAML 1.2 syntax the reader behind sigs.k8s.io/yaml rejects is read. It returns an error where sigs.k8s.io/yaml panics on a nil target. The package documentation lists every difference; check them against what your manifests rely on, in particular numbers read into string fields and keys that print the same. The limits are the default limits of the oneyaml package and cannot be changed, and there is no Untrusted() mode: for input you do not control, read with the oneyaml package.
Move to the oneyaml API
| sigs.k8s.io/yaml | oneyaml |
|---|---|
yaml.Unmarshal(y, &obj) | oneyaml.Unmarshal(y, &obj) |
yaml.UnmarshalStrict(y, &obj) | oneyaml.Unmarshal(y, &obj, oneyaml.KnownFields()); errors.Is(err, oneyaml.ErrUnknownField) matches an unknown field, and duplicate keys are always an error |
yaml.Marshal(obj) | oneyaml.Marshal(obj) |
yaml.YAMLToJSON(y) | oneyaml.Unmarshal into any, then json.Marshal |
yaml.JSONToYAML(j) | oneyaml.Unmarshal(j, &v), then oneyaml.Marshal(v) |
JSONOpt such as UseNumber | not needed: integers above 2^53 stay exact |
Behaviour that differs
- Tags. sigs.k8s.io/yaml reads and writes
jsontags and ignoresyamltags. Theoneyamlpackage uses a field'syamltag and falls back to itsjsontag only when the field has noyamltag. A field that has both reads differently after the move. Types written for Kubernetes carry onlyjsontags and work as they are:
var c struct { Name string `json:"name"` Image string `json:"image,omitempty"` } if err := oneyaml.Unmarshal([]byte("name: web\nimage: nginx:1.25\n"), &c); err != nil { log.Fatal(err) } fmt.Println(c.Name, c.Image) // Output: web nginx:1.25
- Field order. A round trip through JSON sorts struct fields.
Marshalkeeps their declaration order and sorts only map keys.
type Container struct { Name string `json:"name"` Image string `json:"image"` Args []string `json:"args,omitempty"` } out, err := oneyaml.Marshal(Container{Name: "web", Image: "nginx:1.25", Args: []string{"--port", "8080"}}) if err != nil { log.Fatal(err) } fmt.Print(string(out)) // Output: // name: web // image: nginx:1.25 // args: // - --port // - '8080'
- YAML 1.2, not 1.1.
yesandonare strings and0755is the integer 755. A bool field refusesyes, and the error names the path and position:
var cfg struct { Debug bool `yaml:"debug"` } err := oneyaml.Unmarshal([]byte("debug: yes\n"), &cfg) var te *oneyaml.TypeError if errors.As(err, &te) { fmt.Println(te.Path, te.Line, te.Column) } // Output: /debug 1 8
- Every type error is reported. sigs.k8s.io/yaml binds through
encoding/json, which skips a value that does not fit and reports only the earliest. Theoneyamlpackage also skips it and reads the rest, but its*TypeErrorlists every mismatch inMoreandAll(); the read stops at the 100th, and fewer are reported when their paths are very long. A syntax error, a limit error or an error from your own hook stops the read at once and is returned alone. - Merge keys are off unless you pass
MergeKeys(). See Migrating from yaml.v3 for the other 1.1 forms. - Keys keep their type. Into
any, a mapping with a non-string key is amap[any]any; sigs.k8s.io/yaml turns every key into a string. - JSON methods are used, after the YAML and text ones. A type with
UnmarshalJSONorMarshalJSONand no YAML or text method is read and written through them, with the value passed as JSON text, so types written forencoding/json, such as Kubernetes API types, need no change. A value that JSON cannot hold, such as a mapping with a key that is not a string, or.nan, is a*TypeErrorfor such a type. Numbers arrive with their digits as written, on their own or inside a mapping or sequence, so a quantity such as12345678901234567890.5is not rounded. The order differs fromencoding/json: a YAML method (UnmarshalYAML,MarshalYAML) ranks first, and for a scalarUnmarshalTextandMarshalTextrank above the JSON methods, where sigs.k8s.io/yaml calls the JSON methods first.Levelhere has onlyUnmarshalJSONandMarshalJSON:
var cfg struct { Level Level `yaml:"level"` } if err := oneyaml.Unmarshal([]byte("level: high\n"), &cfg); err != nil { log.Fatal(err) } fmt.Println(cfg.Level) out, err := oneyaml.Marshal(cfg) if err != nil { log.Fatal(err) } fmt.Print(string(out)) // The error from UnmarshalJSON comes back inside a *TypeError. err = oneyaml.Unmarshal([]byte("level: loud\n"), &cfg) fmt.Println(err) // Output: // 2 // level: high // oneyaml: line 1, column 8: /level: cannot unmarshal "loud" into oneyaml_test.Level: unknown level "loud"
- JSON is YAML. A JSON document reads as it is, and integers above 2^53 stay exact:
var v map[string]any if err := oneyaml.Unmarshal([]byte(`{"id": 12345678901234567890, "ok": true}`), &v); err != nil { log.Fatal(err) } fmt.Println(v["id"], v["ok"]) // Output: 12345678901234567890 true
The supported Go versions are listed under Stability in the README. For how the libraries compare, see Comparison.