What System.Text.Json considers equal
.NET 8 added JsonNode.DeepEquals and .NET 9 added JsonElement.DeepEquals. They agree on most things, but not on numbers. Every row below was run on .NET 8.0 and .NET 10.0:
| Left | Right | JsonNode, .NET 8 | JsonNode, .NET 10 | JsonElement, .NET 10 |
|---|---|---|---|---|
1.0 | 1 | not equal | equal | equal |
1e2 | 100 | not equal | equal | equal |
-0 | 0 | not equal | equal | equal |
0.1 | 1e-1 | not equal | equal | equal |
1.00000000000000000000000000000000000001 | 1 | not equal | not equal | not equal |
12345678901234567890 | 12345678901234567891 | not equal | not equal | not equal |
{"a":1,"b":2} | {"b":2,"a":1} | equal | equal | equal |
[1,2] | [2,1] | not equal | not equal | not equal |
"\u0061" | "a" | equal | equal | equal |
1 | "1" | not equal | not equal | not equal |
{"a":1,"a":2} | {"a":1,"a":2} | ArgumentException | ArgumentException | equal |
{"a":1,"a":2} | {"a":2,"a":1} | ArgumentException | ArgumentException | not equal |
{"b":0,"a":1,"a":2} | {"a":1,"b":0,"a":2} | ArgumentException | ArgumentException | equal |
- Numbers. On .NET 10 both methods compare the exact decimal value:
1.0,1,1e0and10e-1are all equal, and nothing is rounded to adouble, so a difference 38 digits after the point still counts. .NET 8'sJsonNode.DeepEqualscompares the number text, so any other spelling of the same value is a difference. - Property order is ignored everywhere. Properties are matched by their unescaped name, so
"a"and"a"are the same name. Array order always matters. - Duplicate property names. A
JsonObjectcannot hold two properties with the same name, soJsonNode.DeepEqualsthrowsArgumentException("An item with the same key has already been added") on both versions.JsonElement.DeepEqualsreads the document directly and compares repeated names in the order they appear. - Unreadable input. Comparing a string with half a surrogate pair, such as
"\ud800", can make both methods throwInvalidOperationException, and on .NET 10 comparing a number whose exponent does not fit in anint, such as1e2147483648, throwsArgumentOutOfRangeException. The page tells you when your documents contain either.
Reading the differences
Each difference has a path in the format of JsonNode.GetPath(): $ is the root, .name a property, [0] an array item, and ['a b'] a name with a space, dot, quote, bracket or other character that would be ambiguous (the quotes inside are not escaped).
- changed: the value at the path differs. Objects and arrays are compared inside, so a change deep in a document shows as one line at its own path.
- removed and added: the property or array item exists only on the left or only on the right. With Compare by index, an item inserted at the start of an array changes every item after it; Ignore order matches equal items wherever they are.
- key order (when Ignore key order is off): the shared properties of an object come in a different order. DeepEquals still calls the objects equal.
Values are shown the way JsonNode.ToJsonString writes them with UnsafeRelaxedJsonEscaping: minified, with numbers exactly as written. The table of numbers written differently lists values that .NET 9+ calls equal while .NET 8 does not.
Compare JSON in C#
This code lists the differences exactly like the page does with the default options (arrays by index). On .NET 10 it gives the .NET 9+ list, on .NET 8 the .NET 8 one:
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
static class JsonDiff
{
public static List<string> Compare(string leftJson, string rightJson)
{
var changes = new List<string>();
Compare(JsonNode.Parse(leftJson), JsonNode.Parse(rightJson), "$", changes);
return changes;
}
static void Compare(JsonNode? left, JsonNode? right, string path, List<string> changes)
{
// .NET 9+: numbers are compared by value (1.0 equals 1); .NET 8 compares their text.
// Property order is ignored on both.
if (JsonNode.DeepEquals(left, right)) return;
if (left is JsonObject a && right is JsonObject b)
{
foreach (var (name, value) in a)
{
if (b.TryGetPropertyValue(name, out var other)) Compare(value, other, Child(path, name), changes);
else changes.Add($"removed {Child(path, name)}: {Json(value)}");
}
foreach (var (name, value) in b)
{
if (!a.ContainsKey(name)) changes.Add($"added {Child(path, name)}: {Json(value)}");
}
}
else if (left is JsonArray x && right is JsonArray y)
{
for (int i = 0; i < Math.Max(x.Count, y.Count); i++)
{
if (i >= y.Count) changes.Add($"removed {path}[{i}]: {Json(x[i])}");
else if (i >= x.Count) changes.Add($"added {path}[{i}]: {Json(y[i])}");
else Compare(x[i], y[i], $"{path}[{i}]", changes);
}
}
else
{
changes.Add($"changed {path}: {Json(left)} -> {Json(right)}");
}
}
// The same path format as JsonNode.GetPath().
static string Child(string path, string name) =>
name.IndexOfAny(['.', ' ', '\'', '"', '/', '\\', '[', ']', '(', ')', '\t', '\n', '\r', '\f', '\b', '\u0085', '\u2028', '\u2029']) >= 0
? $"{path}['{name}']"
: $"{path}.{name}";
static string Json(JsonNode? node) => node?.ToJsonString(Relaxed) ?? "null";
static readonly JsonSerializerOptions Relaxed = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
}
If you only need a yes or no on .NET 9 or later, skip JsonNode:
using System.Text.Json;
static bool JsonEquals(string leftJson, string rightJson)
{
using var left = JsonDocument.Parse(leftJson);
using var right = JsonDocument.Parse(rightJson);
// .NET 9+. Numbers by value, property order ignored, repeated names compared in order.
return JsonElement.DeepEquals(left.RootElement, right.RootElement);
}
Both snippets are compiled and run in this page's tests: the list on .NET 8 and .NET 10, the yes or no on .NET 10.
FAQ
How do I compare two JSON documents in C#?
Parse both with JsonNode.Parse and call JsonNode.DeepEquals, or on .NET 9 and later parse them with JsonDocument.Parse and call JsonElement.DeepEquals on the root elements. To see what differs, walk the two trees as in the code above.
Does JsonNode.DeepEquals treat 1.0 and 1 as equal?
On .NET 10 yes, and 1e2 equals 100. On .NET 8 no: it compares the text of each number. Pick the version in Compare numbers like to see the list each one gives.
Does the order of properties matter?
No. JSON objects are unordered and DeepEquals matches properties by name. Untick Ignore key order to see where the order differs anyway.
What happens with duplicate property names?
JsonNode.DeepEquals throws an ArgumentException (An item with the same key has already been added) on .NET 8 and .NET 10. JsonElement.DeepEquals compares them instead: repeated names must appear with equal values in the same order.
Can I compare arrays regardless of order?
Choose Ignore order for arrays: each item is matched with an equal item anywhere in the other array. DeepEquals itself always compares arrays in order.
My JSON has comments. Can I compare it?
Tick Allow comments and trailing commas, the same as JsonDocumentOptions with CommentHandling = JsonCommentHandling.Skip and AllowTrailingCommas = true. The C# above then passes those options to JsonNode.Parse.
Is my JSON uploaded?
No. The comparison happens locally in your browser.