jsonmerge

package module
v0.2.9 Latest Latest
Warning

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

Go to latest
Published: Feb 15, 2026 License: MIT Imports: 3 Imported by: 0

README

JSON Merge Patch Go

Go Version Go Reference

A type-safe, RFC 7386 compliant JSON Merge Patch implementation for Go with generic support.

✨ Features

  • 🔥 RFC 7386 Compliant - Passes all standard test cases from RFC 7386 Appendix A
  • 🚀 Type-Safe Generics - Compile-time type safety with Go 1.18+ generics
  • 🎯 Minimal API - Only 3 core functions: Merge, Generate, Valid
  • Performance Optimized - Zero-copy optimization with optional in-place mutation
  • 🛡️ Production Ready - Tested, immutable by default, thread-safe
  • 📦 Multiple Document Types - Supports structs, maps, JSON bytes, and JSON strings
  • 🌐 Unicode Support - Full Unicode and international character support

🚀 Quick Start

Installation
go get github.com/kaptinlin/jsonmerge
Basic Usage
package main

import (
    "fmt"
    "log"
    "github.com/kaptinlin/jsonmerge"
)

func main() {
    // Original document
    target := map[string]any{
        "title": "Hello World",
        "author": map[string]any{
            "name":  "John Doe",
            "email": "john@example.com",
        },
        "tags": []string{"example", "demo"},
    }

    // JSON Merge Patch (RFC 7386)
    patch := map[string]any{
        "title": "Hello Go",           // Replace
        "author": map[string]any{
            "email": nil,               // Delete email field
        },
        "tags": []string{"go", "json"}, // Replace entire array
        "publishDate": "2024-01-01",    // Add new field
    }

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

    fmt.Printf("%+v\n", result.Doc)
    // Output: map[title:Hello Go author:map[name:John Doe] tags:[go json] publishDate:2024-01-01]
}

📋 API Reference

Core Functions
// Merge applies a JSON Merge Patch to a target document
func Merge[T Document](target, patch T, opts ...Option) (*Result[T], error)

// Generate creates a merge patch between two documents  
func Generate[T Document](source, target T) (T, error)

// Valid checks if a patch is a valid JSON Merge Patch
func Valid[T Document](patch T) bool
Supported Document Types
type Document interface {
    ~[]byte | ~string | map[string]any | any
}
Options
// WithMutate enables in-place modification for performance
func WithMutate(mutate bool) Option

🎯 Use Cases & Examples

1. Type-Safe Struct Merging
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"} // Only update name

result, err := jsonmerge.Merge(user, patch)
// result.Doc is automatically of type User
// Name: "Jane", Email: "john@example.com", Age: 0 (set by patch)
2. Advanced Struct Features
type Config struct {
    Name        string  `json:"name"`
    Port        *int    `json:"port,omitempty"`       // Pointer fields
    Debug       *bool   `json:"debug,omitempty"`      // Nil = omitted
    Description string  `json:"desc,omitempty"`       // omitempty support
    Internal    string  `json:"-"`                    // Ignored fields
    Price       float64 `json:"price,string"`         // Custom JSON tags
}

// Embedded structs are also supported
type Person struct {
    Name string `json:"name"`
    Address struct {
        Street string `json:"street"`
        City   string `json:"city"`
    } `json:"address"`
    Age int `json:"age"`
}
3. Dynamic Map Merging
config := map[string]any{
    "database": map[string]any{
        "host": "localhost",
        "port": 5432,
    },
    "debug": true,
}

update := map[string]any{
    "database": map[string]any{
        "host": "prod-server",  // Update
        "ssl":  true,           // Add
    },
    "debug": nil,               // Delete
}

result, _ := jsonmerge.Merge(config, update)
4. JSON String/Bytes Processing
// JSON strings
target := `{"name":"John","age":30}`
patch := `{"name":"Jane","email":"jane@example.com"}`
result, _ := jsonmerge.Merge(target, patch)

// JSON bytes  
targetBytes := []byte(`{"name":"John","age":30}`)
patchBytes := []byte(`{"name":"Jane","email":"jane@example.com"}`)
result, _ := jsonmerge.Merge(targetBytes, patchBytes)
5. Unicode and International Support
target := `{"name":"José","city":"São Paulo"}`
patch := `{"name":"María","country":"España"}`
result, _ := jsonmerge.Merge(target, patch)
// Full Unicode support for international characters
6. Generate Patches
original := map[string]any{"name": "John", "age": 30, "city": "NYC"}
updated := map[string]any{"name": "Jane", "age": 30, "country": "USA"}

patch, _ := jsonmerge.Generate(original, updated)
// patch: map[name:Jane country:USA city:<nil>]
7. Performance Optimization
// Default: Immutable (safe for concurrent use)
result, _ := jsonmerge.Merge(doc, patch)

// High-performance: In-place mutation (use with caution)
result, _ := jsonmerge.Merge(doc, patch, jsonmerge.WithMutate(true))

🔄 RFC 7386 vs JSON Patch (RFC 6902)

Feature JSON Merge Patch (RFC 7386) JSON Patch (RFC 6902)
Complexity Simple and intuitive Operation-based commands
Learning Curve Low Higher
Array Operations Complete replacement Precise element operations
Delete Operations null values Explicit remove operations
Use Cases Form updates, config changes Precise changes, detailed operations
Example Comparison

Updating: {"name": "John", "age": 30}{"name": "Jane", "age": 30, "email": "jane@example.com"}

JSON Merge Patch (This Library):

{
  "name": "Jane",
  "email": "jane@example.com"
}

JSON Patch:

[
  {"op": "replace", "path": "/name", "value": "Jane"},
  {"op": "add", "path": "/email", "value": "jane@example.com"}
]

Conclusion: JSON Merge Patch is simpler for common scenarios.

Related Tools: For JSON Patch (RFC 6902) operations in Go, see jsonpatch.

📊 Performance

Benchmarks
goos: darwin
goarch: arm64
pkg: github.com/kaptinlin/jsonmerge
cpu: Apple M3

BenchmarkMerge-8                  952150     1357 ns/op    1273 B/op   17 allocs/op
BenchmarkMergeWithMutate-8       2400202      466 ns/op     345 B/op    4 allocs/op
BenchmarkMergeStructs-8           154684     8722 ns/op    3993 B/op   78 allocs/op
BenchmarkMergeJSONStrings-8       246922     5458 ns/op    3743 B/op   77 allocs/op
BenchmarkMergeJSONBytes-8         206040     5934 ns/op    3416 B/op   74 allocs/op
BenchmarkMergeDeepNesting-8       873924     1288 ns/op    2025 B/op   14 allocs/op
BenchmarkMergeLargeArrays-8         1978   625293 ns/op  672759 B/op 10026 allocs/op
Performance Tips
  • Use WithMutate(true) for performance-critical scenarios (up to 3x faster)
  • JSON bytes/strings are more efficient than struct marshaling for large data
  • Immutable mode is safe for concurrent use
  • Deep nesting has minimal performance impact

🧪 Testing & Quality

  • RFC 7386 Compliant - Passes all RFC 7386 Appendix A test cases
  • Edge Cases Covered - Unicode, large numbers, deep nesting, mixed types
  • Go Type System Support - Pointer fields, embedded structs, custom JSON tags
Run Tests
# Run all tests
go test -v

# Run benchmarks
go test -bench=. -run=^$

📚 Examples

Explore comprehensive examples in the examples/ directory:

🔧 Advanced Usage

Custom Types with Generics
type Config struct {
    Database DatabaseConfig `json:"database"`
    Server   ServerConfig   `json:"server"`
}

config := Config{...}
patch := Config{...}

// Type-safe merge with custom struct
result, err := jsonmerge.Merge(config, patch)
// result.Doc is of type Config
Error Handling

The library provides sentinel errors for error checking with errors.Is():

result, err := jsonmerge.Merge(target, patch)
if err != nil {
    switch {
    case errors.Is(err, jsonmerge.ErrMarshal):
        // Handle JSON marshaling errors
    case errors.Is(err, jsonmerge.ErrUnmarshal):
        // Handle JSON unmarshaling errors
    case errors.Is(err, jsonmerge.ErrConversion):
        // Handle type conversion errors
    default:
        // Handle other errors
    }
}

Sentinel Errors:

  • ErrMarshal - JSON marshaling failed
  • ErrUnmarshal - JSON unmarshaling failed
  • ErrConversion - Type conversion failed
Validation
patches := []any{
    map[string]any{"name": "test"},  // Valid
    `{"key": "value"}`,              // Valid JSON string
    []byte(`{"data": true}`),        // Valid JSON bytes
    "simple string",                 // Valid (treated as raw string)
    42,                              // Valid (primitive value)
    nil,                             // Valid (null patch)
}

for _, patch := range patches {
    if jsonmerge.Valid(patch) {
        fmt.Println("Valid patch")
    }
}
Concurrent Usage
// Immutable operations are thread-safe
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        patch := map[string]any{"id": id, "processed": true}
        result, _ := jsonmerge.Merge(originalDoc, patch)
        // Safe to use result.Doc concurrently
    }(i)
}
wg.Wait()

🛡️ Best Practices

  1. Use immutable mode by default for thread safety
  2. Enable mutation only when performance is critical and thread safety is guaranteed
  3. Prefer type-safe struct operations when possible for compile-time safety
  4. Validate patches in public APIs using the Valid function
  5. Handle errors appropriately for production code
  6. Use omitempty tags carefully - they affect merge behavior

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'feat: add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

📄 License

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

Documentation

Overview

Package jsonmerge provides RFC 7386 JSON Merge Patch implementation. It offers a simple, type-safe API for applying merge patches to JSON documents.

Basic usage:

result, err := jsonmerge.Merge(target, patch)
if err != nil {
	return err
}
// result.Doc contains the merged document

The library supports multiple document types:

  • Structs (with full type safety)
  • map[string]any (dynamic documents)
  • []byte (JSON bytes)
  • string (JSON strings)

All operations are immutable by default. Use WithMutate(true) for performance-critical scenarios where in-place modification is acceptable.

types.go defines the core types and functional options for JSON Merge Patch operations.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Generate

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

Generate creates a JSON Merge Patch between source and target documents. The generated patch can be applied to source to produce target.

Possible errors (checkable with errors.Is):

  • ErrMarshal: JSON marshaling failed during type conversion
  • ErrUnmarshal: JSON unmarshaling failed during type conversion
  • ErrConversion: type conversion between document types failed
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 checks if a patch is a valid JSON Merge Patch. According to RFC 7386, any valid JSON value is a valid merge patch.

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

Types

type Document

type Document interface {
	~[]byte | ~string | map[string]any | any
}

Document represents the supported document types for JSON Merge Patch operations. This constraint allows for type-safe operations across different JSON representations.

Supported types and their behavior:

  • []byte: must contain valid JSON; returns ErrUnmarshal if invalid
  • string: first attempts JSON parsing; if invalid JSON, treated as raw string value
  • map[string]any: native format with zero conversion overhead (most efficient)
  • struct types (via any): converted through JSON marshal/unmarshal cycle; respects json struct tags (json:"name,omitempty", json:"-")
  • primitive types (bool, int*, uint*, float*): passed through directly

type Error added in v0.2.4

type Error string

Error represents a sentinel error type for the jsonmerge package.

const (
	// ErrMarshal indicates JSON marshaling failed.
	ErrMarshal Error = "marshal failed"

	// ErrUnmarshal indicates JSON unmarshaling failed.
	ErrUnmarshal Error = "unmarshal failed"

	// ErrConversion indicates type conversion between document types failed.
	ErrConversion Error = "type conversion failed"
)

Sentinel errors for error checking with errors.Is.

func (Error) Error added in v0.2.4

func (e Error) Error() string

type Option

type Option func(*Options)

Option is a functional option type for configuring merge operations.

func WithMutate

func WithMutate(mutate bool) Option

WithMutate configures whether to modify the target document in place. By default, merge operations are immutable and create a new document. Setting mutate to true can improve performance but may affect thread safety.

Default: false

Example:

result, err := Merge(target, patch, WithMutate(true))

type Options

type Options struct {
	// Mutate controls whether to modify the target document in place for performance.
	Mutate bool
}

Options contains configuration for merge operations.

type Result

type Result[T Document] struct {
	// Doc is the merged document, preserving the same type as the input.
	Doc T
}

Result wraps the merged document with type safety. The generic parameter T preserves the original document type through the merge operation.

func Merge

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

Merge applies a JSON Merge Patch (RFC 7386) to a target document. It returns a new Result containing the merged document and metadata. The operation is immutable by default unless WithMutate(true) is specified.

Possible errors (checkable with errors.Is):

  • ErrMarshal: JSON marshaling failed during type conversion
  • ErrUnmarshal: JSON unmarshaling failed during type conversion
  • ErrConversion: type conversion between document types failed
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
map-merge command
struct-merge command

Jump to

Keyboard shortcuts

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