Skip to content

Instantly share code, notes, and snippets.

@reefbarman
Last active May 27, 2025 21:06
Show Gist options
  • Select an option

  • Save reefbarman/b5e1652ad0b9a5298cb40d72867129c4 to your computer and use it in GitHub Desktop.

Select an option

Save reefbarman/b5e1652ad0b9a5298cb40d72867129c4 to your computer and use it in GitHub Desktop.
A quick implementation of http://jsonpatch.com/ to patch json files in C#. Requires MiniJSON https://gist.github.com/darktable/1411710
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
//https://gist.github.com/darktable/1411710
namespace MiniJSON
{
//http://jsonpatch.com/
//Passed all but 1 test at: https://github.com/json-patch/json-patch-tests
//Test not passed was having two 'op' keys in a patch object. MiniJSON just overrides the first with the second
public class JsonPatch
{
private struct PathSection
{
public int index;
public string key;
public string original;
}
public string Patch(string originalJson, string patchJson)
{
var patchesObj = Json.Deserialize(patchJson);
var patches = patchesObj as List<object>;
if (patches == null)
{
throw new FormatException($"JsonPatch root object not a valid json array - {patchJson}");
}
var originalObj = Json.Deserialize(originalJson);
if (originalObj == null)
{
throw new FormatException($"originalJson not a valid json object - {originalJson}");
}
foreach (var patchObj in patches)
{
var patch = patchObj as Dictionary<string, object>;
ValidatePatch(patch);
var op = patch["op"] as string;
try
{
switch (op)
{
case "add":
originalObj = Add(originalObj, patch["path"] as string, patch["value"]);
break;
case "remove":
originalObj = Remove(originalObj, patch["path"] as string);
break;
case "replace":
originalObj = Replace(originalObj, patch["path"] as string, patch["value"]);
break;
case "copy":
originalObj = Copy(originalObj, patch["from"] as string, patch["path"] as string);
break;
case "move":
originalObj = Move(originalObj, patch["from"] as string, patch["path"] as string);
break;
case "test":
originalObj = Test(originalObj, patch["path"] as string, patch["value"]);
break;
}
}
catch (FormatException e)
{
throw new FormatException($"JsonPatch patch object invalid - {e.Message} - patch: {Json.Serialize(patch)}");
}
catch (ArgumentOutOfRangeException e)
{
throw new ArgumentException($"JsonPatch patch failed - {e.Message} - patch: {Json.Serialize(patch)}");
}
catch (ArgumentException e)
{
throw new ArgumentException($"JsonPatch patch path invalid - {e.Message} - patch: {Json.Serialize(patch)}");
}
}
return Json.Serialize(originalObj);
}
private object Add(object originalObj, string path, object value)
{
var pathSections = ParsePath(path);
return Navigate(originalObj, pathSections, delegate(object objectAtPath, PathSection? pathSection) {
object replacementObj = null;
if (pathSection.HasValue)
{
var originalDic = objectAtPath as Dictionary<string, object>;
if (originalDic != null)
{
originalDic[pathSection.Value.key] = value;
replacementObj = originalDic;
}
else
{
var originalArray = objectAtPath as List<object>;
if (originalArray != null)
{
if (pathSection.Value.index < -1 && pathSection.Value.index > originalArray.Count)
{
throw new ArgumentException($"invalid array index: {pathSection.Value.index}");
}
originalArray.Insert(pathSection.Value.index == -1 ? originalArray.Count : pathSection.Value.index, value);
replacementObj = originalArray;
}
else
{
throw new ArgumentException($"path not valid - {pathSection.Value.original}");
}
}
}
else
{
replacementObj = value;
}
return replacementObj;
});
}
private object Remove(object originalObj, string path)
{
var pathSections = ParsePath(path);
return Navigate(originalObj, pathSections, delegate (object objectAtPath, PathSection? pathSection) {
object replacementObj = null;
if (pathSection.HasValue)
{
var originalDic = objectAtPath as Dictionary<string, object>;
if (originalDic != null && originalDic.ContainsKey(pathSection.Value.key))
{
originalDic.Remove(pathSection.Value.key);
replacementObj = originalDic;
}
else
{
var originalArray = objectAtPath as List<object>;
if (originalArray != null && pathSection.Value.index >= 0 && pathSection.Value.index < originalArray.Count)
{
originalArray.RemoveAt(pathSection.Value.index);
replacementObj = originalArray;
}
else
{
throw new ArgumentException($"path not valid - {pathSection.Value.original}");
}
}
}
else
{
throw new ArgumentException("can't remove a scalar");
}
return replacementObj;
});
}
private object Replace(object originalObj, string path, object value)
{
var pathSections = ParsePath(path);
return Navigate(originalObj, pathSections, delegate (object objectAtPath, PathSection? pathSection) {
object replacementObj = null;
if (pathSection.HasValue)
{
var originalDic = objectAtPath as Dictionary<string, object>;
if (originalDic != null && originalDic.ContainsKey(pathSection.Value.key))
{
originalDic[pathSection.Value.key] = value;
replacementObj = originalDic;
}
else
{
var originalArray = objectAtPath as List<object>;
if (originalArray != null && pathSection.Value.index >= 0 && pathSection.Value.index < originalArray.Count)
{
originalArray[pathSection.Value.index] = value;
replacementObj = originalArray;
}
else
{
throw new ArgumentException($"path not valid - {pathSection.Value.original}");
}
}
}
else
{
replacementObj = value;
}
return replacementObj;
});
}
private object Test(object originalObj, string path, object value)
{
var pathSections = ParsePath(path);
return Navigate(originalObj, pathSections, delegate (object objectAtPath, PathSection? pathSection) {
object replacementObj = null;
if (pathSection.HasValue)
{
var originalDic = objectAtPath as Dictionary<string, object>;
if (originalDic != null && originalDic.ContainsKey(pathSection.Value.key))
{
if (!CompareValues(value, originalDic[pathSection.Value.key]))
{
var message = $"expected: {value} got: {originalDic[pathSection.Value.key]}";
throw new ArgumentOutOfRangeException(message);
}
replacementObj = originalDic;
}
else
{
var originalArray = objectAtPath as List<object>;
if (originalArray != null && pathSection.Value.index >= 0 && pathSection.Value.index < originalArray.Count)
{
if (!CompareValues(value, originalArray[pathSection.Value.index]))
{
var message = $"expected: {value} got: {originalArray[pathSection.Value.index]}";
throw new ArgumentOutOfRangeException(message);
}
replacementObj = originalArray;
}
else
{
throw new ArgumentException($"path not valid - {pathSection.Value.original}");
}
}
}
else
{
if (!CompareValues(value, originalObj))
{
var message = $"expected: {value} got: {originalObj}";
throw new ArgumentOutOfRangeException(message);
}
}
return replacementObj;
});
}
private object Move(object originalObj, string from, string to)
{
var pathSections = ParsePath(from);
object valueToMove = null;
var removed = Navigate(originalObj, pathSections, delegate (object objectAtPath, PathSection? pathSection) {
object replacementObj = null;
if (pathSection.HasValue)
{
var originalDic = objectAtPath as Dictionary<string, object>;
if (originalDic != null && originalDic.ContainsKey(pathSection.Value.key))
{
valueToMove = originalDic[pathSection.Value.key];
originalDic.Remove(pathSection.Value.key);
replacementObj = originalDic;
}
else
{
var originalArray = objectAtPath as List<object>;
if (originalArray != null && pathSection.Value.index >= 0 && pathSection.Value.index < originalArray.Count)
{
valueToMove = originalArray[pathSection.Value.index];
originalArray.RemoveAt(pathSection.Value.index);
replacementObj = originalArray;
}
else
{
throw new ArgumentException($"path not valid - {pathSection.Value.original}");
}
}
}
else
{
throw new ArgumentException("can't move a scalar");
}
return replacementObj;
});
return Add(removed, to, valueToMove);
}
private object Copy(object originalObj, string from, string to)
{
var pathSections = ParsePath(from);
object valueToCopy = null;
originalObj = Navigate(originalObj, pathSections, delegate (object objectAtPath, PathSection? pathSection) {
object replacementObj = null;
if (pathSection.HasValue)
{
var originalDic = objectAtPath as Dictionary<string, object>;
if (originalDic != null && originalDic.ContainsKey(pathSection.Value.key))
{
valueToCopy = originalDic[pathSection.Value.key];
replacementObj = originalDic;
}
else
{
var originalArray = objectAtPath as List<object>;
if (originalArray != null && pathSection.Value.index >= 0 && pathSection.Value.index < originalArray.Count)
{
valueToCopy = originalArray[pathSection.Value.index];
replacementObj = originalArray;
}
else
{
throw new ArgumentException($"path not valid - {pathSection.Value.original}");
}
}
}
else
{
throw new ArgumentException("can't move a scalar");
}
return replacementObj;
});
return Add(originalObj, to, valueToCopy);
}
private object Navigate(object originalObj, PathSection[] paths, Func<object, PathSection?, object> onNavigationComplete)
{
if (paths.Length > 1)
{
var key = paths[0].key;
var originalDic = originalObj as Dictionary<string, object>;
if (originalDic != null && originalDic.ContainsKey(key))
{
originalDic[key] = Navigate(originalDic[key], paths.SubArray(1, paths.Length - 1), onNavigationComplete);
return originalDic;
}
else
{
var index = paths[0].index;
var originalArray = originalObj as List<object>;
if (originalArray != null && index >= 0 && index < originalArray.Count)
{
originalArray[index] = Navigate(originalArray[index], paths.SubArray(1, paths.Length - 1), onNavigationComplete);
return originalArray;
}
else
{
throw new ArgumentException($"path not valid - {paths[0].original}");
}
}
}
else if (paths.Length == 1)
{
return onNavigationComplete(originalObj, paths[0]);
}
else
{
return onNavigationComplete(originalObj, null);
}
}
private PathSection[] ParsePath(string path)
{
List<PathSection> sections = new List<PathSection>();
if (path != null)
{
var pathParts = path.Split('/');
for (var i = 1; i < pathParts.Length; i++)
{
var part = pathParts[i];
PathSection section = new PathSection();
section.original = part;
int index;
if (Int32.TryParse(part, out index) && IsDigitsOnly(part) && !(part.Length > 1 && part[0] == '0'))
{
section.key = part;
section.index = index < 0 ? -99 : index;
}
else if (part == "-")
{
section.key = null;
section.index = -1;
}
else
{
var replacements = new Dictionary<string, string>() {
{"~0", "~"},
{"~1", "/"}
};
var regex = new Regex(String.Join("|", replacements.Keys.Select(k => Regex.Escape(k)).ToArray()));
section.key = regex.Replace(part, m => replacements[m.Value]);
section.index = -99;
}
sections.Add(section);
}
}
else
{
throw new FormatException($"invalid path format - {path}");
}
return sections.ToArray();
}
private void ValidatePatch(Dictionary<string, object> patch)
{
bool success = false;
if (patch != null && patch.ContainsKey("op") && patch.ContainsKey("path"))
{
var op = patch["op"] as string;
switch (op)
{
case "add":
success = patch.ContainsKey("value");
break;
case "remove":
success = true;
break;
case "replace":
success = patch.ContainsKey("value");
break;
case "copy":
success = patch.ContainsKey("from");
break;
case "move":
success = patch.ContainsKey("from");
break;
case "test":
success = patch.ContainsKey("value");
break;
}
}
if (!success)
{
throw new FormatException($"JsonPatch patch object not a valid json object - {Json.Serialize(patch)}");
}
}
private bool CompareValues(object a, object b)
{
if (a == null && b == null)
{
return true;
}
if (a is IComparable && b is IComparable)
{
return ((IComparable) a).CompareTo((IComparable) b) == 0;
}
if (a is IDictionary && b is IDictionary)
{
var aDic = a as Dictionary<string, object>;
var bDic = b as Dictionary<string, object>;
if (aDic.Keys.Count == bDic.Keys.Count)
{
bool valid = true;
foreach (var keyPair in aDic)
{
if (bDic.ContainsKey(keyPair.Key))
{
valid &= CompareValues(keyPair.Value, bDic[keyPair.Key]);
}
else
{
valid = false;
}
if (!valid)
{
break;
}
}
return valid;
}
}
if (a is IList && b is IList)
{
var aList = a as List<object>;
var bList = b as List<object>;
if (aList.Count == bList.Count)
{
bool valid = true;
for (var i = 0; i < aList.Count; i++)
{
valid &= CompareValues(aList[i], bList[i]);
if (!valid)
{
break;
}
}
return valid;
}
}
return false;
}
private bool IsDigitsOnly(string str)
{
foreach (char c in str)
{
if (c < '0' || c > '9')
return false;
}
return true;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment