Skip to content

Instantly share code, notes, and snippets.

@cajuncoding
Last active March 8, 2026 00:23
Show Gist options
  • Select an option

  • Save cajuncoding/bf78bdcf790782090d231590cbc2438f to your computer and use it in GitHub Desktop.

Select an option

Save cajuncoding/bf78bdcf790782090d231590cbc2438f to your computer and use it in GitHub Desktop.
Simple Merge process for JsonNode using System.Text.Json
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace CajunCoding
{
public static class SystemTextJsonMergeExtensions
{
/// <summary>
/// Merges the specified Json Node into the base JsonNode for which this method is called.
/// It is null safe and can be easily used with null-check & null coalesce operators for fluent calls.
/// NOTE: JsonNodes are context aware and track their parent relationships therefore to merge the values both JsonNode objects
/// specified are mutated. The Base is mutated with new data while the source is mutated to remove reverences to all
/// fields so that they can be added to the base.
/// This is unfortunately an unavoidable behavior of System.Text.Json. Therefore, I've opted to keep the mutating
/// behavior in place to optimize for performance as the default behavior. And, it can be easily resolved by the caller
/// simply calling `jsonNode.DeepClone()` prior to the merge -- providing full control to the caller.
///
/// Source taken directly from the open-source Gist here:
/// https://gist.github.com/cajuncoding/bf78bdcf790782090d231590cbc2438f
///
/// </summary>
/// <param name="jsonBase"></param>
/// <param name="jsonMerge"></param>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public static JsonNode Merge(this JsonNode jsonBase, JsonNode jsonMerge, bool mergeIfAlreadyExists = true)
{
if (jsonBase == null || jsonMerge == null)
return jsonBase;
switch (jsonBase)
{
case JsonObject jsonBaseObj when jsonMerge is JsonObject jsonMergeObj:
{
//NOTE: We must materialize the set (e.g. to an Array), and then clear the merge array so the node can then be
// re-assigned to the target/base Json; clearing the Object seems to be the most efficient approach...
var mergeNodesArray = jsonMergeObj.ToArray();
jsonMergeObj.Clear();
foreach (var prop in mergeNodesArray)
{
if(mergeIfAlreadyExists || !jsonBaseObj.ContainsKey(prop.Key))
jsonBaseObj[prop.Key] = jsonBaseObj[prop.Key] switch
{
JsonObject jsonBaseChildObj when prop.Value is JsonObject jsonMergeChildObj => jsonBaseChildObj.Merge(jsonMergeChildObj),
JsonArray jsonBaseChildArray when prop.Value is JsonArray jsonMergeChildArray => jsonBaseChildArray.Merge(jsonMergeChildArray),
_ => prop.Value
};
}
break;
}
case JsonArray jsonBaseArray when jsonMerge is JsonArray jsonMergeArray:
{
//NOTE: We must materialize the set (e.g. to an Array), and then clear the merge array,
// so they can then be re-assigned to the target/base Json...
var mergeNodesArray = jsonMergeArray.ToArray();
jsonMergeArray.Clear();
foreach (var mergeNode in mergeNodesArray) jsonBaseArray.Add(mergeNode);
break;
}
default:
throw new ArgumentException($"The JsonNode type [{jsonBase.GetType().Name}] is incompatible for merging with the target/base " +
$"type [{jsonMerge.GetType().Name}]; merge requires the types to be the same.");
}
return jsonBase;
}
/// <summary>
/// Merges the specified Dictionary of values into the base JsonNode for which this method is called.
///
/// Source taken directly from the open-source Gist here:
/// https://gist.github.com/cajuncoding/bf78bdcf790782090d231590cbc2438f
///
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TValue"></typeparam>
/// <param name="jsonBase"></param>
/// <param name="dictionary"></param>
/// <param name="options"></param>
/// <returns></returns>
public static JsonNode MergeDictionary<TKey, TValue>(this JsonNode jsonBase, IDictionary<TKey, TValue> dictionary, JsonSerializerOptions options = null, bool mergeIfAlreadyExists = true)
=> jsonBase.Merge(dictionary.ToJsonNode(options), mergeIfAlreadyExists);
}
}
@cajuncoding
Copy link
Author

@ossi-pesonen-alfame yes this is unfortunately a limitation of System.Text.Json. I've opted to keep the mutating behavior because it offers (significantly) superior performance. And, as you noted, can be easily resolved by the caller by simply calling jsonNode.DeepClone() prior to the merge providing control to the caller.

Per your comment, I'm not sure what you mean by "and moved the merge functionality into a private method there so performance isn't impacted". Performance is impacted no matter what due to the time it takes to do the DeepClone; so I still think that should be left to the caller as you have done for yourself 😁.

Also this behavior was highlighted in the method comments:

        /// NOTE: JsonNodes are context aware and track their parent relationships therefore to merge the values both JsonNode objects
        ///         specified are mutated. The Base is mutated with new data while the source is mutated to remove reverences to all
        ///         fields so that they can be added to the base.

But, perhaps it'd be valuable to expand a little on the decision reasoning in the method comment πŸ‘.

@cajuncoding
Copy link
Author

πŸš€ This library has been super useful on a number of projects and therefore I've now added/included it in my new Repo I've just published SystemTextJsonHelpers which is also available on Nuget - SystemTextJsonHelpers so it can now be easily pulled in and used on all of my projects.

Hopefully others find it just as useful too... if so then please give the Github Repo a Star 🌟 (it's free, and it'll help others find the project)!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment