jsonmerge

package module
v0.2.23 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 4, 2026 License: MIT Imports: 7 Imported by: 1

README

JSON Merge Patch for Go

Go Module Go Reference

A type-safe RFC 7386 JSON Merge Patch library for Go that preserves the caller's document type

Features

  • RFC 7386 semantics: Object patches merge recursively, null deletes fields, non-object patches replace the target, and arrays replace as a whole
  • Type preservation: Merge and Generate return the same document type they receive
  • Flexible document forms: Work with map[string]any, JSON []byte, JSON string, structs, and scalar values
  • Safe-by-default map merges: Preserve map[string]any inputs unless you opt into WithMutate(true)
  • Small API: Learn Merge, Generate, Valid, and WithMutate
  • Benchmarked and tested: Includes RFC Appendix A coverage, benchmarks, and runnable examples

Installation

go get github.com/kaptinlin/jsonmerge

Requires Go 1.26.3 or newer.

Quick Start

package main

import (
	"fmt"
	"log"

	"github.com/kaptinlin/jsonmerge"
)

func main() {
	target := map[string]any{"name": "John", "age": 30}
	patch := map[string]any{"age": 31, "email": "john@example.com"}

	result, err := jsonmerge.Merge(target, patch)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result.Doc["name"])
	fmt.Println(result.Doc["age"])
	fmt.Println(result.Doc["email"])
}

Document Forms

Form Behavior
map[string]any Canonical in-memory object form
[]byte Must contain valid JSON
string Parsed as JSON when valid; otherwise treated as a raw string scalar
Structs and typed Go values Converted through JSON before merge or generation
Scalars and nil Accepted when they fit the call's static type T

Invalid JSON bytes fail. Invalid JSON strings remain valid raw string values.

Core Operations

API Description
Merge[T Document](target, patch T, opts ...Option) Apply a merge patch and return *Result[T]
Generate[T Document](source, target T) Build a merge patch that transforms source into target
Valid[T Document](patch T) Report whether a value is accepted as a patch input
WithMutate(true) Allow in-place updates for map[string]any targets during object merges

Struct Patch Semantics

Struct patches follow JSON marshaling rules. Zero values overwrite fields once they are present in the marshaled patch.

type User struct {
	Name  string `json:"name"`
	Email string `json:"email,omitempty"`
	Age   int    `json:"age"`
}

user := User{Name: "John", Email: "john@example.com", Age: 30}
patch := User{Name: "Jane"}

result, _ := jsonmerge.Merge(user, patch)
fmt.Println(result.Doc.Name)  // Jane
fmt.Println(result.Doc.Email) // john@example.com
fmt.Println(result.Doc.Age)   // 0

Use pointer fields or map[string]any when you need omission-versus-zero-value distinction.

Error Handling

The package wraps failures with sentinel errors so callers can use errors.Is:

  • ErrMarshal
  • ErrUnmarshal
  • ErrConversion

Invalid JSON bytes return an unmarshal error. Invalid JSON strings are accepted as raw string scalar values.

Examples

Run the example programs from the repository root:

go run ./examples/map-merge
go run ./examples/struct-merge
go run ./examples/json-string-merge
go run ./examples/json-bytes-merge

See examples/ for complete programs.

Performance

WithMutate(true) reduces allocations for map[string]any object merges by allowing in-place updates. Run benchmarks on your hardware with:

task bench

Development

task test          # Run all tests with race detection
task lint          # Run golangci-lint and tidy checks
task verify        # Run deps, fmt, vet, lint, test, and vuln

For development guidelines and package contracts, see AGENTS.md.

Contributing

Contributions are welcome. See CONTRIBUTING.md for contribution guidelines.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Documentation

Overview

Package jsonmerge implements RFC 7386 JSON Merge Patch for Go.

It supports structs, map[string]any, []byte, and string documents. Use WithMutate(true) to allow in-place updates when the caller prefers speed over preserving map inputs.

See https://datatracker.ietf.org/doc/html/rfc7386.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrMarshal indicates JSON marshaling failed.
	ErrMarshal = errors.New("marshal failed")

	// ErrUnmarshal indicates JSON unmarshaling failed.
	ErrUnmarshal = errors.New("unmarshal failed")

	// ErrConversion indicates type conversion between document types failed.
	ErrConversion = errors.New("type conversion failed")
)

Functions

func Generate

func Generate[T Document](source, target T) (T, error)

Generate returns a merge patch that transforms source into target. If it fails, the error matches ErrMarshal, ErrUnmarshal, or ErrConversion.

Example
package main

import (
	"fmt"
	"log"

	"github.com/kaptinlin/jsonmerge"
)

func main() {
	source := map[string]any{"name": "John", "age": 30, "city": "NYC"}
	target := map[string]any{"name": "Jane", "age": 30}

	patch, err := jsonmerge.Generate(source, target)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(patch["name"])
	fmt.Println(patch["city"])
}
Output:
Jane
<nil>

func Valid

func Valid[T Document](patch T) bool

Valid reports whether patch is accepted as a JSON Merge Patch value.

Example
package main

import (
	"fmt"

	"github.com/kaptinlin/jsonmerge"
)

func main() {
	fmt.Println(jsonmerge.Valid(map[string]any{"name": "Jane"}))
	fmt.Println(jsonmerge.Valid([]byte(`{"name": "Jane"}`)))
	fmt.Println(jsonmerge.Valid([]byte(`{invalid}`)))
}
Output:
true
true
false
Example (RawString)
package main

import (
	"fmt"

	"github.com/kaptinlin/jsonmerge"
)

func main() {
	fmt.Println(jsonmerge.Valid("raw string"))
	fmt.Println(jsonmerge.Valid([]byte(`{invalid}`)))
}
Output:
true
false

Types

type Document

type Document interface {
	any
}

Document is the type constraint used by Merge, Generate, and Valid.

type Option

type Option func(*Options)

Option configures Options.

func WithMutate

func WithMutate(mutate bool) Option

WithMutate allows Merge to update map targets in place. The default is false.

type Options

type Options struct {
	Mutate bool
}

Options configures merge behavior.

type Result

type Result[T Document] struct {
	Doc T
}

Result holds a merged document.

func Merge

func Merge[T Document](target, patch T, opts ...Option) (*Result[T], error)

Merge applies patch to target according to RFC 7386. By default it preserves map targets; use WithMutate(true) to update them in place. If it fails, the error matches ErrMarshal, ErrUnmarshal, or ErrConversion.

Example
package main

import (
	"fmt"
	"log"

	"github.com/kaptinlin/jsonmerge"
)

func main() {
	target := map[string]any{"name": "John", "age": 30}
	patch := map[string]any{"age": 31}

	result, err := jsonmerge.Merge(target, patch)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(result.Doc["name"])
	fmt.Println(result.Doc["age"])
}
Output:
John
31
Example (WithMutate)
package main

import (
	"fmt"
	"log"

	"github.com/kaptinlin/jsonmerge"
)

func main() {
	target := map[string]any{"name": "John", "age": 30}
	patch := map[string]any{"age": 31}

	result, err := jsonmerge.Merge(target, patch, jsonmerge.WithMutate(true))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(result.Doc["age"])
}
Output:
31

Directories

Path Synopsis
examples
json-bytes-merge command
Package main demonstrates byte-slice JSON Merge Patch usage.
Package main demonstrates byte-slice JSON Merge Patch usage.
json-string-merge command
Package main demonstrates string-based JSON Merge Patch usage.
Package main demonstrates string-based JSON Merge Patch usage.
map-merge command
Package main demonstrates map-based JSON Merge Patch usage.
Package main demonstrates map-based JSON Merge Patch usage.
struct-merge command
Package main demonstrates struct-based JSON Merge Patch usage.
Package main demonstrates struct-based JSON Merge Patch usage.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL