Shouldly.Json was created by, and is maintained
by Marvin Mall.
It provides extension methods for the Shouldly library to compare JSON strings.
The package can be found on nuget.org. You can install the package with:
Install-Package Shouldly.JsonThe library provides multiple methods for comparing JSON strings.
Use ShouldBeValidJson() to validate if the given string is valid JSON syntax. Be aware that besides arrays and objects, JSON also supports other types like strings, numbers, booleans, and null in the root of a string.
var json = @"{""name"": ""John"", ""age"": 30}";
json.ShouldBeValidJson();Use ShouldBeJsonObject() and ShouldBeJsonArray() to check if the given string is a valid JSON object or array.
var json1 = @"{""name"": ""John"", ""age"": 30}";
var json2 = @"[1, 2, 3]";
json1.ShouldBeJsonObject();
json2.ShouldBeJsonArray();Use ShouldNotBeValidJson(), ShouldNotBeJsonObject() and ShouldNotBeJsonArray() for the opposite assertions. A null or empty string is not valid JSON, so it satisfies the negated assertions.
"{invalid json}".ShouldNotBeValidJson();
"[1, 2, 3]".ShouldNotBeJsonObject();
@"{""name"": ""John""}".ShouldNotBeJsonArray();Note: methods taking a
jsonPointerargument default to the root pointer"", not"/". Pointers accept both standard notation (/path/to/array) and URI fragment notation (#/path/to/array).
Use ShouldBeSemanticallySameJson() to compare two JSON strings for semantic equality. This means:
- Property order in objects doesn't matter
- Whitespace and formatting doesn't matter
- Numbers with same value but different representation (e.g., 1.0 and 1.00) are considered equal
- Array order matters
var json1 = @"{""name"": ""John"", ""age"": 30}";
var json2 = @"{""age"": 30, ""name"": ""John""}";
var json3 = @"{""name"": ""John"", ""age"": 31}";
json1.ShouldBeSemanticallySameJson(json2); // Succeeds
json1.ShouldBeSemanticallySameJson(json3); // FailsUse ShouldNotBeSemanticallySameJson() to assert that two JSON structures differ. Failure messages of the semantic comparison report the JSON pointer path of the first difference found.
var json1 = @"{""name"": ""John"", ""age"": 30}";
var json2 = @"{""name"": ""Jane"", ""age"": 30}";
json1.ShouldNotBeSemanticallySameJson(json2); // Succeeds
json1.ShouldNotBeSemanticallySameJson(json1); // Fails
json1.ShouldBeSemanticallySameJson(json2); // Fails: "JSON value mismatch at path '/name'"Use ShouldBeJsonSubtreeOf() to verify that one JSON structure is contained within another. This means:
- The actual JSON must be a subset of the expected JSON
- Missing properties in objects are allowed
- Arrays must match exactly (including length and order), but objects in arrays are handled like other objects
var json1 = @"{""name"": ""John""}";
var json2 = @"{""name"": ""John"", ""age"": 30}";
var json3 = @"{""name"": ""Adrian""}";
var json4 = @"{""firstName"": ""John""}";
json1.ShouldBeJsonSubtreeOf(json2); // Succeeds
json1.ShouldBeJsonSubtreeOf(json3); // Fails
json1.ShouldBeJsonSubtreeOf(json4); // FailsUse ShouldHaveJsonSubtree() to check the reverse direction: the expected JSON must be a subtree of the actual JSON.
var json1 = @"{""name"": ""John""}";
var json2 = @"{""name"": ""John"", ""age"": 30}";
json2.ShouldHaveJsonSubtree(json1); // Succeeds
json1.ShouldHaveJsonSubtree(json2); // FailsUse ShouldNotBeJsonSubtreeOf() and ShouldNotHaveJsonSubtree() for the negated assertions.
var json1 = @"{""name"": ""John""}";
var json2 = @"{""name"": ""John"", ""age"": 30}";
json1.ShouldNotBeJsonSubtreeOf(json1); // Fails (every JSON is a subtree of itself)
json2.ShouldNotBeJsonSubtreeOf(json1); // Succeeds (json2 has an extra property)
json2.ShouldNotHaveJsonSubtree(json1); // Fails (json1 is present in json2)
json1.ShouldNotHaveJsonSubtree(json2); // SucceedsUse ShouldMatchJsonSchema() to validate a JSON string against a JSON Schema. This allows you to verify that JSON data conforms to an expected structure and format.
var json = @"{""name"": ""John"", ""age"": 30}";
var schema = @"{
""type"": ""object"",
""properties"": {
""name"": { ""type"": ""string"" },
""age"": { ""type"": ""integer"", ""minimum"": 0 }
},
""required"": [""name"", ""age""]
}";
json.ShouldMatchJsonSchema(schema);Use ShouldNotMatchJsonSchema() to assert that a JSON string violates a schema.
@"{""name"": 42}".ShouldNotMatchJsonSchema(schema); // Succeeds
@"{""name"": ""John""}".ShouldNotMatchJsonSchema(schema); // FailsUse ShouldHaveJsonProperty() to check if a JSON string has a specific property. You can also provide a JSON pointer to check the existence of a property at a specific path.
var json = @"{""user"": {""name"": ""John""}}";
json.ShouldHaveJsonProperty("/user/name");Use ShouldNotHaveJsonProperty() to assert that a property does not exist. Note that a property with a null value does exist.
var json = @"{""user"": {""name"": ""John"", ""age"": null}}";
json.ShouldNotHaveJsonProperty("/user/email"); // Succeeds
json.ShouldHaveJsonProperty("/user/age"); // Succeeds (the property exists, value is null)Use ShouldHaveJsonValueType() to check the JSON type of the value at a path, or the named shortcuts for the six JSON types. Booleans are checked as a single type (JsonValueKind.True and JsonValueKind.False are both booleans). null is a value kind of its own, distinct from a missing pointer.
var json = @"{""name"": ""John"", ""age"": 30, ""active"": true, ""meta"": {""id"": 1}, ""tags"": [""a""], ""deletedAt"": null}";
json.ShouldHaveJsonValueType("/name", JsonValueKind.String);
json.ShouldBeJsonStringAt("/name");
json.ShouldBeJsonNumberAt("/age");
json.ShouldBeJsonBooleanAt("/active");
json.ShouldBeJsonObjectAt("/meta");
json.ShouldBeJsonArrayAt("/tags");
json.ShouldHaveJsonNullValue("/deletedAt");
json.ShouldNotBeJsonStringAt("/age");
json.ShouldNotHaveJsonValueType("/name", JsonValueKind.Number);
json.ShouldNotHaveJsonNullValue("/name");Use ShouldHaveJsonValue() to check if a JSON string has a specific property with a specific value. You can also provide a JSON pointer to check the value of a property at a specific path.
var json1 = @"{""user"": {""name"": ""John""}}";
var json2 = @"{""users"": [{""name"": ""John""}, {""name"": ""Jane""}]}";
json1.ShouldHaveJsonValue("/user/name", "John");
json2.ShouldHaveJsonValue("/users/2/name", "Jane");Use any of the other ShouldHaveJsonValue methods to check the value of a property at a specific path with different comparators.
var json = @"{""user"": {""name"": ""John"", ""age"": 30}}";
json.ShouldHaveJsonValueLessThan("/user/age", 31);
json.ShouldHaveJsonValueLessThanOrEqualTo("/user/age", 30);
json.ShouldHaveJsonValueGreaterThan("/user/age", 29);
json.ShouldHaveJsonValueGreaterThanOrEqualTo("/user/age", 30);
json.ShouldHaveJsonValueBetween("/user/age", 29, 31);Use ShouldNotHaveJsonValue(), ShouldNotHaveJsonValueBetween() and ShouldHaveJsonValueOneOf() / ShouldNotHaveJsonValueOneOf() for negated and set-based comparisons. A pointer that does not exist satisfies the negated assertions.
var json = @"{""user"": {""name"": ""John"", ""status"": ""active"", ""age"": 30}}";
json.ShouldNotHaveJsonValue("/user/name", "Jane");
json.ShouldNotHaveJsonValueBetween("/user/age", 40, 50);
json.ShouldHaveJsonValueOneOf("/user/status", new[] { "active", "pending" });
json.ShouldNotHaveJsonValueOneOf("/user/status", new[] { "deleted", "archived" });
json.ShouldNotHaveJsonValue("/user/name", "john", StringComparer.OrdinalIgnoreCase); // Fails (case-insensitive match)Special methods for date/time values are also available. They work very similar but provide better semantics for date/time values. Also be aware that those methods use the upper bound exclusively while the other methods inclusively.
var json = @"{""user"": {""name"": ""John"", ""birthDate"": ""1990-01-01""}}";
json.ShouldHaveJsonDateBefore("/user/birthDate", new DateTime(1990, 1, 2));
json.ShouldHaveJsonDateBeforeOrEqualTo("/user/birthDate", new DateTime(1990, 1, 2));
json.ShouldHaveJsonDateAfter("/user/birthDate", new DateTime(1990, 1, 1));
json.ShouldHaveJsonDateAfterOrEqualTo("/user/birthDate", new DateTime(1990, 1, 1));
json.ShouldHaveJsonDateBetween("/user/birthDate", new DateTime(1989, 1, 1), new DateTime(1991, 1, 1));Use ShouldNotHaveJsonDateBetween() for the negated range check (same bounds: start inclusive, end exclusive).
json.ShouldNotHaveJsonDateBetween("/user/birthDate", new DateTime(1980, 1, 1), new DateTime(1985, 1, 1));Use ShouldHaveJsonValueMatchingRegex() and ShouldNotHaveJsonValueMatchingRegex() to validate that string values at specific JSON pointer paths match or don't match regular expression patterns.
var json = @"{
""name"": ""John"",
""email"": ""john@example.com"",
""phone"": ""555-1234"",
""invalid_email"": ""not-an-email""
}";
// Assert that values match specific patterns
json.ShouldHaveJsonValueMatchingRegex("/name", @"^[A-Z][a-z]+$");
json.ShouldHaveJsonValueMatchingRegex("/email", @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$");
json.ShouldHaveJsonValueMatchingRegex("/phone", @"^\d{3}-\d{4}$");
// Assert that values do NOT match patterns
json.ShouldNotHaveJsonValueMatchingRegex("/invalid_email", @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$");
json.ShouldNotHaveJsonValueMatchingRegex("/name", @"^[a-z]+$"); // Should not be all lowercaseShouldAnyHaveJsonValueMatchingRegex() and ShouldAnyNotHaveJsonValueMatchingRegex() apply a pattern to a property of the elements of a JSON array, requiring at least one match (or at least one non-match) respectively.
var json = @"{""users"": [{""email"": ""a@b.com""}, {""email"": ""invalid""}]}";
json.ShouldAnyHaveJsonValueMatchingRegex("/users", "/email", @"^[^@]+@[^@]+\.[^@]+$"); // Succeeds
json.ShouldAnyNotHaveJsonValueMatchingRegex("/users", "/email", @"^\d+$"); // SucceedsUse ShouldHaveJsonArrayCount() to check the length of an array. The jsonPointer argument is optional and defaults to the root pointer "", so omitting it checks an array in the root of the JSON string.
var json1 = @"[1, 2, 3]";
var json2 = @"{""users"": [""John"", ""Jane""]}";
json1.ShouldHaveJsonArrayCount(3); // root array, pointer omitted
json2.ShouldHaveJsonArrayCount(2, "/users"); // nested arrayShouldNotHaveJsonArrayCount() is the exact inverse: it takes the same optional jsonPointer and fails precisely when ShouldHaveJsonArrayCount() would succeed, so omitting the pointer there also means "the root array".
var json1 = "[1, 2, 3]";
var json2 = @"{""users"": [""John"", ""Jane""]}";
json1.ShouldNotHaveJsonArrayCount(2); // inverse of ShouldHaveJsonArrayCount(2) on the root array
json1.ShouldHaveJsonArrayCount(3); // the positive counterpart succeeds
json2.ShouldNotHaveJsonArrayCount(3, "/users"); // inverse with an explicit pointerRange-based length assertions are available too, with the same default root pointer and inclusive bounds.
var json1 = "[1, 2, 3]";
var json2 = @"{""users"": [""John"", ""Jane""]}";
json1.ShouldHaveJsonArrayCountBetween(2, 4); // 2, 3 or 4 elements
json1.ShouldNotHaveJsonArrayCountBetween(5, 8);
json2.ShouldBeJsonArrayNotEmpty("/users");
json2.ShouldNotBeJsonArrayEmpty("/users");
@"{""users"": []}".ShouldBeJsonArrayEmpty("/users");Use ShouldContainJsonElement() / ShouldNotContainJsonElement() to check whether a JSON array contains an element semantically equal to the given JSON. Elements are compared with the same semantics as ShouldBeSemanticallySameJson(): property order is ignored, array order matters, and 42 equals 42.0.
var json = @"{""users"": [{""name"": ""John"", ""age"": 30}, {""name"": ""Jane""}]}";
json.ShouldContainJsonElement("/users", @"{""age"": 30, ""name"": ""John""}"); // Succeeds
json.ShouldNotContainJsonElement("/users", @"{""name"": ""Bob""}"); // SucceedsUse ShouldHaveJsonArrayUniqueElements() to assert that an array contains no semantically duplicate elements; duplicates are reported with their indices.
"[1, 2, 3]".ShouldHaveJsonArrayUniqueElements();
"[1, 2, 1]".ShouldHaveJsonArrayUniqueElements(); // Fails: "duplicate elements at indices: 2"
"[42, 42.0]".ShouldHaveJsonArrayUniqueElements(); // Fails: numerically equal
"[1, 1, 2]".ShouldNotHaveJsonArrayUniqueElements(); // SucceedsUse ShouldAllHaveJsonProperty() / ShouldAllNotHaveJsonProperty() to assert that every element of a JSON array has (or does not have) a property, and ShouldAnyHaveJsonProperty() / ShouldNotAnyHaveJsonProperty() for the "at least one element" variants. Failures report the offending indices.
var json = @"{""users"": [{""name"": ""John"", ""age"": 30}, {""name"": ""Jane"", ""age"": 25}]}";
json.ShouldAllHaveJsonProperty("/users", "/name");
json.ShouldAllNotHaveJsonProperty("/users", "/email");
json.ShouldAnyHaveJsonProperty("/users", "/age");
json.ShouldNotAnyHaveJsonProperty("/users", "/email");Use the ShouldAllHaveJsonValue* / ShouldAllHaveJsonDate* family to compare the value of a property of every array element, and the ShouldAllNotHaveJsonValue* / ShouldAllNotHaveJsonDate* family for negated checks. ShouldAnyHaveJsonValue() and ShouldAllNotHaveJsonValue() check whether at least one element matches a value, and whether no element matches it.
var json = @"{""users"": [{""age"": 30}, {""age"": 25}, {""age"": 40}]}";
json.ShouldAllHaveJsonValueGreaterThan("/users", "/age", 20);
json.ShouldAllHaveJsonValueBetween("/users", "/age", 20, 50);
json.ShouldAllNotHaveJsonValueBetween("/users", "/age", 50, 60);
json.ShouldAllNotHaveJsonValue("/users", "/age", 35);
json.ShouldAnyHaveJsonValue("/users", "/age", 25);The date variants work the same way, with the upper bound exclusive:
var json = @"{""users"": [{""created"": ""2024-01-02""}, {""created"": ""2024-01-03""}]}";
json.ShouldAllHaveJsonDateAfter("/users", "/created", new DateTime(2024, 1, 1));
json.ShouldAllNotHaveJsonDateBetween("/users", "/created", new DateTime(2024, 2, 1), new DateTime(2024, 3, 1));This library is open-sourced software licensed under the MIT license.