JSON-to-YAML in Go is mostly a one-liner once you pick a library. The choice is about output style — gopkg.in/yaml.v3 produces clean, modern YAML, sigs.k8s.io/yaml produces Kubernetes-style output. Both are reliable. The API is useful when you want to avoid the dependency entirely.
Method 1: gopkg.in/yaml.v3 (the canonical option)
yaml.v3 is the most widely-used YAML library in the Go ecosystem. It supports YAML 1.2, custom indentation, and round-trips JSON cleanly.
go get gopkg.in/yaml.v3
package main
import (
"encoding/json"
"fmt"
"os"
"gopkg.in/yaml.v3"
)
func jsonToYAML(inPath, outPath string) error {
in, err := os.ReadFile(inPath)
if err != nil {
return fmt.Errorf("read: %w", err)
}
var data interface{}
if err := json.Unmarshal(in, &data); err != nil {
return fmt.Errorf("json: %w", err)
}
out, err := os.Create(outPath)
if err != nil {
return err
}
defer out.Close()
enc := yaml.NewEncoder(out)
enc.SetIndent(2)
defer enc.Close()
return enc.Encode(data)
}
func main() {
if err := jsonToYAML("config.json", "config.yaml"); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
The flow: parse JSON into interface{}, encode that to YAML. SetIndent(2) matches the most common YAML style; use 4 if your team prefers it. For nested arrays under map keys, yaml.v3 produces correctly-indented output without configuration.
Method 2: sigs.k8s.io/yaml (Kubernetes-style output)
sigs.k8s.io/yaml is a thin wrapper that uses ghodss/yaml's JSON-tag-based round-tripping. It's what kubectl uses, so output matches Kubernetes manifests stylistically.
go get sigs.k8s.io/yaml
package main
import (
"fmt"
"os"
"sigs.k8s.io/yaml"
)
func jsonToYAML(inPath, outPath string) error {
in, err := os.ReadFile(inPath)
if err != nil {
return err
}
out, err := yaml.JSONToYAML(in)
if err != nil {
return fmt.Errorf("convert: %w", err)
}
return os.WriteFile(outPath, out, 0o644)
}
func main() {
if err := jsonToYAML("deployment.json", "deployment.yaml"); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
This is the cleanest API of the three. yaml.JSONToYAML takes raw JSON bytes and returns YAML bytes — no intermediate interface{}, no Encoder setup. Use this for Kubernetes manifest generation or anywhere you want kubectl-compatible output.
Method 3: ChangeThisFile API (no dep, no config)
If you don't want to add a YAML library to your binary or you're processing untrusted input where you'd rather not deserialize it locally, the API does it. Free tier covers 1,000 conversions/month.
package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
const apiKey = "ctf_sk_your_key_here"
func jsonToYAML(inPath, outPath string) error {
body := &bytes.Buffer{}
w := multipart.NewWriter(body)
f, err := os.Open(inPath)
if err != nil {
return err
}
defer f.Close()
fw, err := w.CreateFormFile("file", "input.json")
if err != nil {
return err
}
if _, err := io.Copy(fw, f); err != nil {
return err
}
_ = w.WriteField("source", "json")
_ = w.WriteField("target", "yaml")
_ = w.Close()
req, err := http.NewRequest("POST", "https://changethisfile.com/v1/convert", body)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", w.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(resp.Body)
return fmt.Errorf("api %d: %s", resp.StatusCode, msg)
}
out, err := os.Create(outPath)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
return err
}
func main() {
if err := jsonToYAML("config.json", "config.yaml"); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
The API output uses 2-space indentation and standard YAML 1.2 quoting. For Kubernetes-compatible output, pass style=k8s in the form data.
When to use each
| Approach | Best for | Tradeoff |
|---|---|---|
| gopkg.in/yaml.v3 | General-purpose YAML output, custom indentation | Slightly more setup than k8s yaml |
| sigs.k8s.io/yaml | Kubernetes manifests, kubectl-compatible output | Less configurable than yaml.v3 |
| ChangeThisFile API | Edge runtimes, no dep on YAML library | Network call, per-call cost above free tier |
CLI alternatives: yq
For one-off conversions or shell pipelines, yq (the Go-based one by mikefarah) is hard to beat:
apt install yq # or brew install yq
yq -p json -o yaml config.json > config.yaml
cat config.json | yq -p json -o yaml
// From Go, shell out via os/exec:
cmd := exec.Command("yq", "-p", "json", "-o", "yaml", "config.json")
out, err := cmd.Output()
yq is the go-to CLI. From within a Go service, use yaml.v3 directly — shelling out to yq adds latency without any benefit.
Production tips
- Use yaml.v3, not yaml.v2. v2 doesn't preserve key ordering and uses spec-deviating quoting. v3 is the maintained line and produces output matching modern YAML expectations.
- For Kubernetes manifests, set apiVersion/kind/metadata first. If you're generating YAML to feed kubectl, consider using sigs.k8s.io/yaml with typed K8s API structs from k8s.io/api — JSON-tag round-tripping preserves the ordering you'd expect.
- Watch for nil vs empty. A JSON null becomes YAML
nullin yaml.v3. An absent JSON field becomes nothing. If you want "present but empty", emit an empty string explicitly. - Anchors and aliases. If your YAML needs
&ref/*refreuse, you have to construct *yaml.Node trees manually — JSON has no equivalent so the conversion can't infer them.
For most Go services, gopkg.in/yaml.v3 with a four-line round-trip is the right answer. For Kubernetes work, sigs.k8s.io/yaml. For everything else, the API. Free tier covers 1,000 conversions/month.