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

ApproachBest forTradeoff
gopkg.in/yaml.v3General-purpose YAML output, custom indentationSlightly more setup than k8s yaml
sigs.k8s.io/yamlKubernetes manifests, kubectl-compatible outputLess configurable than yaml.v3
ChangeThisFile APIEdge runtimes, no dep on YAML libraryNetwork 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 null in 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 / *ref reuse, 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.