Last active
October 10, 2024 20:17
-
-
Save cajuncoding/bf78bdcf790782090d231590cbc2438f to your computer and use it in GitHub Desktop.
Simple Merge process for JsonNode using System.Text.Json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | |
/// | |
/// 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) | |
{ | |
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) | |
{ | |
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. | |
/// </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) | |
=> jsonBase.Merge(JsonSerializer.SerializeToNode(dictionary, options)); | |
} | |
} |
Fix Log:
07/11/2024: As noted in this response here, there was a missing case for nested properties of an object that were actually an array that wasn't being handled, so that is now updated.
For future reference here is the Unit Test that validates a variety of common use cases . . .
using System.Text.Json;
using System.Text.Json.Serialization;
using CajunCoding;
namespace SystemTextJsonMergeExtensionTests
{
[TestFixture]
public class SystemTextJsonMergeExtensionTests
{
[JsonConverter(typeof(JsonStringEnumMemberConverter))]
private enum StarWarsCharacterType
{
Civilian,
Jedi,
StormTrooper,
Droid,
SithLord
}
[JsonConverter(typeof(JsonStringEnumMemberConverter))]
private enum LightsaberColor { Blue, Green, Yellow }
[Test]
public void TestSystemTextJsonObjectMerge()
{
var jedi = new
{
Name = "Luke Skywalker",
CharacterType = StarWarsCharacterType.Jedi,
LightsaberCount = -1, //Should get overwritten...
//Test merging of nested objects...
DroidFriends = new[]
{
new { Name = "R2D2", CharacterType = StarWarsCharacterType.Droid }
},
Enemies = new
{
DarthVader = StarWarsCharacterType.SithLord
}
};
var jediAugmentingData = new
{
LightsaberOriginalOwner = "Anikan Skywalker",
LightsaberCount = 3,
OriginalLightsaberColor = LightsaberColor.Blue,
ReplacementLightsaberColor = LightsaberColor.Green,
FoundLighSaberColor = LightsaberColor.Yellow,
DroidFriends = new[]
{
new { Name = "C-3PO", CharacterType = StarWarsCharacterType.Droid }
},
Enemies = new
{
SenatorPalpatine = StarWarsCharacterType.SithLord
}
};
var jediJson = JsonSerializer.SerializeToNode(jedi);
var jediAugmentingDataJson = JsonSerializer.SerializeToNode(jediAugmentingData);
var json = jediJson.Merge(jediAugmentingDataJson);
//NOTE: This assumes that the Macross.JsonExtensions StringEnumConverter is used on the Enums...
Assert.AreEqual("Jedi", json["CharacterType"]?.GetValue<string>());
Assert.AreEqual("Anikan Skywalker", json["LightsaberOriginalOwner"]?.GetValue<string>());
Assert.AreEqual(3, json["LightsaberCount"]?.GetValue<int>());
Assert.AreEqual("Blue", json["OriginalLightsaberColor"]?.GetValue<string>());
Assert.AreEqual("Green", json["ReplacementLightsaberColor"]?.GetValue<string>());
Assert.AreEqual(2, json["DroidFriends"]?.AsArray()?.Count);
foreach (var droidFriend in json["DroidFriends"]?.AsArray())
{
Assert.AreEqual("Droid", droidFriend["CharacterType"].GetValue<string>());
}
Assert.AreEqual(2, json["Enemies"]?.AsObject().ToArray().Length);
foreach (var enemyProp in json["Enemies"]?.AsObject())
{
Assert.AreEqual("SithLord", enemyProp.Value.GetValue<string>());
}
}
}
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The feature gap between
System.Text.Json
andNewtonsoft.Json
regarding support for dynamically and easily merging Json objects is discussed further in this (still open) GitHub issue: dotnet/runtime#31433This solution addresses some of the main use cases with a reasonably small amount of code...