---
title: "Apply a JSON Patch to a YAML file"
url: https://recursiveai.org/oneyaml/articles/json-patch-yaml/
description: "Apply a JSON Patch (RFC 6902) to a YAML file without converting it to JSON, so comments, quotes and key order stay. Code in Go, Rust and C#, plus merge patches."
date: 2026-10-26
author: oneyaml team
---

# Apply a JSON Patch to a YAML file

Apply a JSON Patch (RFC 6902) to a YAML file without converting it to JSON, so comments, quotes and key order stay. Code in Go, Rust and C#, plus merge patches.

October 26, 2026 · oneyaml team

A JSON Patch (RFC 6902) describes changes to a document’s data, not to its JSON text, so it applies to YAML as well. The common way to do it converts the YAML to JSON, patches the JSON, and converts back, which loses every comment and reorders keys. Applying the patch to the YAML document itself changes only the values the patch names. oneyaml does that in Go, Rust and C#, atomically: if any operation fails, the file is left exactly as it was.

## The file and the patch

*values.yaml*

```
# values.yaml for the web service
image:
  repository: registry.example.com/web
  tag: "1.4.2"   # set by CI
 
replicas: 2      # scaled by the release job
 
# Hosts served by the ingress
hosts:
  - example.com
  - www.example.com
```

The patch checks the current tag with `test`, then bumps it, scales `replicas` and appends a host. `/hosts/-` is RFC 6901 for the end of the list:

*patch.json*

```
[
  { "op": "test", "path": "/image/tag", "value": "1.4.2" },
  { "op": "replace", "path": "/image/tag", "value": "1.5.0" },
  { "op": "replace", "path": "/replicas", "value": 3 },
  { "op": "add", "path": "/hosts/-", "value": "api.example.com" }
]
```

## The usual route: through JSON

In Go this is often done with sigs.k8s.io/yaml, which converts between YAML and JSON, and evanphx/json-patch, which applies the patch, following each project’s README:

*main.go · sigs.k8s.io/yaml v1.6.0 · json-patch v5.9.11*

```
package main
 
import (
    "fmt"
    "log"
    "os"
 
    jsonpatch "github.com/evanphx/json-patch/v5"
    "sigs.k8s.io/yaml"
)
 
func main() {
    src, err := os.ReadFile("values.yaml")
    if err != nil {
        log.Fatal(err)
    }
    patchJSON, err := os.ReadFile("patch.json")
    if err != nil {
        log.Fatal(err)
    }
 
    // YAML to JSON, patch the JSON, JSON back to YAML.
    doc, err := yaml.YAMLToJSON(src)
    if err != nil {
        log.Fatal(err)
    }
    patch, err := jsonpatch.DecodePatch(patchJSON)
    if err != nil {
        log.Fatal(err)
    }
    modified, err := patch.Apply(doc)
    if err != nil {
        log.Fatal(err)
    }
    out, err := yaml.JSONToYAML(modified)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Print(string(out))
}
```

*Output*

```
hosts:
- example.com
- www.example.com
- api.example.com
image:
  repository: registry.example.com/web
  tag: 1.5.0
replicas: 3
```

The data is right, but the file is not the one you started with: the four comments and both blank lines are gone, the keys are in alphabetical order, the quotes around the tag are dropped, and the list is no longer indented under `hosts`. Commit that, and the diff covers the whole file. This is the trip any JSON-based route takes, because JSON has no comments.

## Apply the patch to the YAML itself

Each program reads the same two files, applies the patch, prints the result, and then applies the patch a second time to show the `test` guard at work.

### Go

*main.go · oneyaml.dev/go v1.0.0*

```
package main
 
import (
    "fmt"
    "log"
    "os"
 
    oneyaml "oneyaml.dev/go"
)
 
func main() {
    src, err := os.ReadFile("values.yaml")
    if err != nil {
        log.Fatal(err)
    }
    patch, err := os.ReadFile("patch.json")
    if err != nil {
        log.Fatal(err)
    }
    doc, err := oneyaml.Parse(src)
    if err != nil {
        log.Fatal(err)
    }
 
    if err := doc.ApplyPatch(patch); err != nil {
        log.Fatal(err)
    }
    fmt.Print(doc)
 
    // Run it again: the test fails, and nothing changes.
    before := doc.String()
    err = doc.ApplyPatch(patch)
    fmt.Printf("second run: %v; unchanged: %v\n", err, doc.String() == before)
}
```

*Output*

```
# values.yaml for the web service
image:
  repository: registry.example.com/web
  tag: "1.5.0"   # set by CI
 
replicas: 3      # scaled by the release job
 
# Hosts served by the ingress
hosts:
  - example.com
  - www.example.com
  - api.example.com
second run: oneyaml: patch operation 0 (test "/image/tag"): patch test failed; unchanged: true
```

### Rust

*main.rs · oneyaml 1.2.0*

```
use oneyaml::{Document, Patch};
 
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut doc = Document::parse(&std::fs::read_to_string("values.yaml")?)?;
    let patch = Patch::from_json(&std::fs::read_to_string("patch.json")?)?;
 
    doc.apply_patch(&patch)?;
    print!("{doc}");
 
    // Run it again: the test fails, and nothing changes.
    let before = doc.to_string();
    let err = doc.apply_patch(&patch).err().ok_or("expected the test to fail")?;
    println!("second run refused at op {:?}; unchanged: {}", err.op_index(), doc.to_string() == before);
    Ok(())
}
```

*Output*

```
# values.yaml for the web service
image:
  repository: registry.example.com/web
  tag: "1.5.0"   # set by CI
 
replicas: 3      # scaled by the release job
 
# Hosts served by the ingress
hosts:
  - example.com
  - www.example.com
  - api.example.com
second run refused at op Some(0); unchanged: true
```

### C#

*Program.cs · OneYaml 1.2.1 · .NET 10*

```
using OneYaml;
 
using var doc = YamlDocument.Parse(File.ReadAllText("values.yaml"));
string patch = File.ReadAllText("patch.json");
 
doc.ApplyPatch(patch);
Console.Write(doc);
 
// Run it again: the test fails, and nothing changes.
string before = doc.ToString();
try
{
    doc.ApplyPatch(patch);
}
catch (YamlException e)
{
    Console.WriteLine($"second run refused at op {e.OperationIndex}; unchanged: {doc.ToString() == before}");
}
```

*Output*

```
# values.yaml for the web service
image:
  repository: registry.example.com/web
  tag: "1.5.0"   # set by CI
 
replicas: 3      # scaled by the release job
 
# Hosts served by the ingress
hosts:
  - example.com
  - www.example.com
  - api.example.com
second run refused at op 0; unchanged: True
```

All three print the same YAML: three values changed, and every comment, blank line, quote and key position kept. The second run fails at operation 0 in all three, because the tag is no longer `1.4.2`, and the document is unchanged. That makes a patch with a `test` safe to run twice, for example from a CI job that may retry.

To install: `go get oneyaml.dev/go@v1.0.0`, `cargo add oneyaml@1.2.0`, or `dotnet add package OneYaml --version 1.2.1`.

## Where YAML rules differ from JSON

The patch runs against the YAML data model, so a few operations behave differently from a JSON patch library:

- **`test` compares YAML values.** The integer `1` and the float `1.0` are different values, where RFC 6902 on JSON would call them equal.
- **Writes through an alias are refused**, because they would change every place that shares the anchored node. Patch the anchor instead.
- **In Rust and C#, `move` and `copy` don’t carry comments.** The value is written without the comments on its line; C# writes it in the style of its new place. To move an entry with its comments in Rust, use `NodeMut::move_entry` ([edit YAML without losing comments](https://recursiveai.org/oneyaml/articles/edit-yaml-keep-comments/)).
- **A failed patch changes nothing.** The error names the operation that failed: `op_index()` in Rust, `OperationIndex` in C#, and the error text in Go.

Each language lists every difference: [Go patches](https://recursiveai.org/oneyaml/go/docs/patches/), [Rust patches and pointers](https://recursiveai.org/oneyaml/rust/docs/guide/#patches-and-pointers) and [C# patches](https://recursiveai.org/oneyaml/csharp/docs/patches/).

## Merge patches for environment overrides

For overlays, such as a production file that changes a few values of a base file, a JSON merge patch (RFC 7396) is often simpler than a list of operations: objects merge key by key, `null` removes a key, and any other value, a list included, replaces what is there. The same documents apply it: `ApplyMergePatch` in Go and C#, `apply_merge_patch` in Rust, with the same layout-keeping result. Each language’s recipe shows an environment overlay: [Go](https://recursiveai.org/oneyaml/go/cookbook/patch-yaml/), [Rust](https://recursiveai.org/oneyaml/rust/cookbook/patch-yaml/), [C#](https://recursiveai.org/oneyaml/csharp/cookbook/patch-yaml/).

## JSON Patch, kubectl and kustomize

kubectl and kustomize also accept JSON Patch, but they apply it to Kubernetes objects: `kubectl patch` updates an API object in the cluster ([Kubernetes docs](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/)), and kustomize patches resources as it builds them ([kustomize example](https://github.com/kubernetes-sigs/kustomize/blob/master/examples/jsonpatch.md)). Neither changes the YAML file in your repository. When the file itself is what you want to change, and a person will review the diff, patch the document as shown above.

To pick a library for your language, see the guides for [Go](https://recursiveai.org/oneyaml/articles/golang-yaml/), [Rust](https://recursiveai.org/oneyaml/articles/rust-yaml-parser/) and [C#](https://recursiveai.org/oneyaml/articles/csharp-yaml-parser/), or start from the [oneyaml home page](https://recursiveai.org/oneyaml/).
