JSON Merge Patch (RFC 7386)¶
Glaze provides full support for JSON Merge Patch (RFC 7386), a format for describing modifications to JSON documents. JSON Merge Patch works with glz::generic for runtime JSON manipulation.
Include¶
When to Use JSON Merge Patch¶
JSON Merge Patch is simpler than JSON Patch (RFC 6902) but has some limitations:
| Use Case | Recommendation |
|---|---|
| Partial update with simple values | Merge Patch |
| Need to set explicit null values | JSON Patch |
| Array element manipulation | JSON Patch |
| Move/copy operations | JSON Patch |
| Simple API for consumers | Merge Patch |
| Conditional updates (test operation) | JSON Patch |
Basic Usage¶
Applying a Merge Patch¶
Use glz::merge_patch() to apply a merge patch to a JSON value:
auto target = glz::read_json<glz::generic>(R"({"a": 1, "b": 2})");
auto patch = glz::read_json<glz::generic>(R"({"a": 99, "c": 3})");
auto ec = glz::merge_patch(*target, *patch);
if (!ec) {
// target is now {"a": 99, "b": 2, "c": 3}
}
Removing Keys with Null¶
A null value in the patch removes the corresponding key:
auto target = glz::read_json<glz::generic>(R"({"a": 1, "b": 2, "c": 3})");
auto patch = glz::read_json<glz::generic>(R"({"b": null})");
glz::merge_patch(*target, *patch);
// target is now {"a": 1, "c": 3}
Generating a Merge Patch¶
Use glz::merge_diff() to generate a merge patch that transforms one document into another:
auto source = glz::read_json<glz::generic>(R"({"a": 1, "b": 2})");
auto target = glz::read_json<glz::generic>(R"({"a": 1, "c": 3})");
auto patch = glz::merge_diff(*source, *target);
if (patch) {
// patch is {"b": null, "c": 3}
// - "b": null means remove key "b"
// - "c": 3 means add key "c" with value 3
}
Using Merge Patch with C++ Structs¶
In addition to working with glz::generic, merge patch can be applied directly to C++ structs. This leverages Glaze's automatic reflection to apply partial updates to your types.
Implementation Note: Glaze's
read_jsonnaturally has merge-patch semantics for structs—it only updates fields present in the JSON and leaves other fields unchanged. Themerge_patchfunction for structs leverages this directly, making it highly efficient with no intermediate serialization or data structure conversions.
Basic Struct Patching¶
struct Person {
std::string name;
int age;
std::string city;
};
Person person{.name = "Alice", .age = 30, .city = "NYC"};
// Apply a partial update - only change the age
auto patch = glz::read_json<glz::generic>(R"({"age": 31})");
auto ec = glz::merge_patch(person, *patch);
if (!ec) {
// person.name is still "Alice"
// person.age is now 31
// person.city is still "NYC"
}
Patching from JSON String¶
You can apply a patch directly from a JSON string:
Person person{.name = "Bob", .age = 25, .city = "LA"};
auto ec = glz::merge_patch(person, R"({"city": "San Francisco"})");
// person.city is now "San Francisco"
Non-Mutating Struct Patching¶
Use glz::merge_patched() to get a new struct without modifying the original:
Person original{.name = "Charlie", .age = 40, .city = "Boston"};
auto result = glz::merge_patched(original, R"({"age": 41})");
if (result) {
// original.age is still 40
// result->age is 41
}
Generating Patches Between Structs¶
Use glz::merge_diff() to compute the difference between two struct instances:
Person before{.name = "Dave", .age = 35, .city = "Seattle"};
Person after{.name = "Dave", .age = 36, .city = "Portland"};
auto patch = glz::merge_diff(before, after);
if (patch) {
// patch contains: {"age": 36, "city": "Portland"}
// "name" is not in the patch because it didn't change
}
// Get the patch as a JSON string
auto patch_json = glz::merge_diff_json(before, after);
// *patch_json is: {"age":36,"city":"Portland"}
Round-Trip with Structs¶
You can generate a patch and apply it to transform one struct into another:
struct Config {
std::string host = "localhost";
int port = 8080;
bool enabled = true;
std::vector<std::string> tags;
};
Config source{.host = "localhost", .port = 8080, .enabled = true, .tags = {"dev"}};
Config target{.host = "production.example.com", .port = 443, .enabled = true, .tags = {"prod"}};
// Generate patch
auto patch = glz::merge_diff(source, target);
// Apply patch to transform source into target
Config result = source;
glz::merge_patch(result, *patch);
// result now matches target:
// result.host == "production.example.com"
// result.port == 443
// result.tags == {"prod"}
Nested Structs¶
Merge patch works with nested structures:
struct Address {
std::string street;
std::string city;
};
struct Employee {
std::string name;
Address address;
};
Employee emp{
.name = "Eve",
.address = {.street = "123 Main St", .city = "Boston"}
};
// Update just the city in the nested address
auto ec = glz::merge_patch(emp, R"({"address": {"city": "NYC"}})");
// emp.address.street is still "123 Main St"
// emp.address.city is now "NYC"
Note: When patching nested objects, the entire nested object in the patch is merged with the target. If you want to completely replace a nested object, include all its fields in the patch.
Struct API Reference¶
// Apply patch to struct (in-place)
template <class T>
[[nodiscard]] error_ctx merge_patch(T& target, const generic& patch);
// Apply patch from JSON string to struct (in-place)
template <class T>
[[nodiscard]] error_ctx merge_patch(T& target, std::string_view patch_json);
// Apply patch to struct (non-mutating)
template <class T>
[[nodiscard]] expected<T, error_ctx> merge_patched(const T& target, const generic& patch);
// Apply patch from JSON string (non-mutating)
template <class T>
[[nodiscard]] expected<T, error_ctx> merge_patched(const T& target, std::string_view patch_json);
// Generate patch between two structs
template <class T>
[[nodiscard]] expected<generic, error_ctx> merge_diff(const T& source, const T& target);
// Generate patch between two structs as JSON string
template <class T>
[[nodiscard]] expected<std::string, error_ctx> merge_diff_json(const T& source, const T& target);
Key Semantics¶
Objects Are Merged Recursively¶
When both the target and patch have nested objects at the same key, they are merged recursively:
auto target = glz::read_json<glz::generic>(R"({
"user": {"name": "Alice", "age": 30}
})");
auto patch = glz::read_json<glz::generic>(R"({
"user": {"age": 31, "city": "NYC"}
})");
glz::merge_patch(*target, *patch);
// target is now:
// {"user": {"name": "Alice", "age": 31, "city": "NYC"}}
Arrays Are Replaced Entirely¶
Unlike JSON Patch, Merge Patch cannot modify individual array elements:
auto target = glz::read_json<glz::generic>(R"({"tags": ["a", "b", "c"]})");
auto patch = glz::read_json<glz::generic>(R"({"tags": ["x"]})");
glz::merge_patch(*target, *patch);
// target is now {"tags": ["x"]} - complete replacement
Non-Object Patches Replace the Target¶
If the patch is not an object, it completely replaces the target:
auto target = glz::read_json<glz::generic>(R"({"a": 1, "b": 2})");
auto patch = glz::read_json<glz::generic>(R"(42)");
glz::merge_patch(*target, *patch);
// target is now 42
API Reference¶
merge_patch¶
Apply a merge patch in-place:
merge_patched¶
Apply a merge patch, returning a new value (non-mutating):
[[nodiscard]] expected<generic, error_ctx> merge_patched(
const generic& target,
const generic& patch
);
merge_diff¶
Generate a merge patch that transforms source into target:
[[nodiscard]] expected<generic, error_ctx> merge_diff(
const generic& source,
const generic& target
);
Convenience Overloads¶
// Apply patch from JSON string
[[nodiscard]] error_ctx merge_patch(
generic& target,
std::string_view patch_json
);
// Apply patch from strings, return JSON string
[[nodiscard]] expected<std::string, error_ctx> merge_patch_json(
std::string_view target_json,
std::string_view patch_json
);
// Apply patch from strings, return generic
[[nodiscard]] expected<generic, error_ctx> merge_patched(
std::string_view target_json,
std::string_view patch_json
);
// Generate patch from strings, return JSON string
[[nodiscard]] expected<std::string, error_ctx> merge_diff_json(
std::string_view source_json,
std::string_view target_json
);
Limitations¶
Cannot Set Explicit Null Values¶
RFC 7386 uses null to mean "remove this key", so you cannot use merge patch to set a value to null. This is documented in the RFC:
"This design means that merge patch documents are suitable for describing modifications to JSON documents that primarily use objects for their structure and do not make use of explicit null values."
For use cases requiring explicit null values, use JSON Patch (RFC 6902) instead.
Round-Trip Limitation¶
When generating a merge patch with merge_diff(), documents containing explicit null values cannot be perfectly reconstructed:
auto source = glz::read_json<glz::generic>(R"({"a": 1})");
auto target = glz::read_json<glz::generic>(R"({"a": null})");
auto patch = glz::merge_diff(*source, *target);
// patch is {"a": null}
auto result = *source;
glz::merge_patch(result, *patch);
// result is {} - the key "a" was removed, not set to null
HTTP Integration¶
When using JSON Merge Patch with HTTP, use the MIME type application/merge-patch+json:
// Example with HTTP client
auto response = client.patch(url, patch_json, {
{"Content-Type", "application/merge-patch+json"}
});
RFC 7386 Appendix A Example¶
This example from RFC 7386 demonstrates all key behaviors:
auto target = glz::read_json<glz::generic>(R"({
"title": "Goodbye!",
"author": {
"givenName": "John",
"familyName": "Doe"
},
"tags": ["example", "sample"],
"content": "This will be unchanged"
})");
auto patch = glz::read_json<glz::generic>(R"({
"title": "Hello!",
"phoneNumber": "+01-123-456-7890",
"author": {
"familyName": null
},
"tags": ["example"]
})");
glz::merge_patch(*target, *patch);
// Result:
// {
// "title": "Hello!", // replaced
// "author": {
// "givenName": "John" // preserved
// // familyName removed
// },
// "tags": ["example"], // replaced entirely
// "content": "This will be unchanged", // preserved
// "phoneNumber": "+01-123-456-7890" // added
// }
Error Handling¶
glz::merge_patch() returns an error_ctx. Possible errors:
| Error Code | Description |
|---|---|
parse_error |
Invalid JSON in string input |
exceeded_max_recursive_depth |
Recursion depth exceeded the compile-time limit (256) |
Unlike JSON Patch, merge patch operations cannot fail due to missing paths or type mismatches - the algorithm handles all cases by design.
See Also¶
- JSON Patch (RFC 6902) - More expressive patching with explicit operations
- Generic JSON - Working with
glz::generic