Skip to main content

Serialization

Describes the possible expectations for the JSON serialization of objects with System.Text.Json.

ExpectationNegatedSummary
IsJsonSerializablesurvives a round trip through the JSON serializer
IsJsonSerializedAsserialized by the JSON serializer as the expected JSON

JSON serializable​

You can verify that an object is JSON serializable:

public record Track(string Title, string Artist);

Track track = new("Let It Be", "The Beatles");

await Expect.That(track).IsJsonSerializable();

This verifies that the object can be serialized to JSON and deserialized again, and that the result is equivalent to the original object. For example, a property with a private setter loses its value:

public class PlayCounter
{
public string Title { get; set; } = "";
public int PlayCount { get; private set; }
public void Play() => PlayCount++;
}

PlayCounter counter = new() { Title = "Let It Be" };
counter.Play();

await Expect.That(counter).IsJsonSerializable();
Failure message
Expected that counter
is serializable as JSON,
but it was not:
Property PlayCount differed:
Actual: 0
Expected: 1

You can specify both the JsonSerializerOptions and the equivalency options:

await Expect.That(track).IsJsonSerializable(
new JsonSerializerOptions { IncludeFields = true },
e => e.IgnoringMember(nameof(Track.Artist)));

You can also specify the type that the subject is deserialized into:

object track = new Track("Let It Be", "The Beatles");

await Expect.That(track).IsJsonSerializable<Track>(
new JsonSerializerOptions { IncludeFields = true },
e => e.IgnoringMember(nameof(Track.Artist)));

Serialized as JSON​

You can verify the JSON that an object is serialized to by System.Text.Json, e.g. to test the contract of a public API:

public enum Genre { Pop = 1, Rock = 2 }
public record Track(string Title, Genre Genre, string? Album = null);

Track track = new("Let It Be", Genre.Rock);
JsonSerializerOptions serializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) },
};

await Expect.That(track).IsJsonSerializedAs("""{"title": "Let It Be", "genre": "rock"}""", serializerOptions);
await Expect.That(track).IsJsonSerializedAs(new { title = "Let It Be", genre = "rock" }, serializerOptions);

With new JsonSerializerOptions(JsonSerializerDefaults.Web) instead, the first expectation would fail with:

Failure message
Expected that track
is serialized as JSON {"title": "Let It Be", "genre": "rock"},
but it differed as
$.genre was number 2 instead of "rock" and
$.album had unexpected <null>

Serialized:
{"title":"Let It Be","genre":2,"album":null}

Unlike IsJsonSerializable, which verifies that an object survives a round trip through the serializer, IsJsonSerializedAs verifies the JSON itself: the property names and their casing, the representation of enums, omitted null values and the effect of converters.

The expected value is either a JSON string, which must be valid JSON (otherwise an ArgumentException is thrown), or an object that is compared like in Matches, including It.Is… expectations. Without JsonSerializerOptions, the subject is serialized with the default options of the customization. A null subject is serialized as null, and a subject that cannot be serialized, e.g. because of a reference cycle, fails the expectation with the message of the exception.

The serialized JSON is compared exactly, like in MatchesExactly, so additional properties fail the expectation. All JsonOptions are available to relax the comparison, e.g. IgnoringAdditionalProperties(), IgnoringArrayOrder(), TreatingNullAsAbsent() or IgnoringMember(name):

await Expect.That(track).IsJsonSerializedAs(new { title = "Let It Be" }, serializerOptions,
o => o.IgnoringAdditionalProperties());
Trimming and Native AOT

Unless a JsonTypeInfo<T> is passed, the subject is serialized by reflection, which is switched off when publishing with trimming or Native AOT. In such an application, pass the source-generated JsonTypeInfo<T> of the subject together with the expected value as a JSON string:

[JsonSerializable(typeof(Track))]
internal partial class TrackContext : JsonSerializerContext;

await Expect.That(track).IsJsonSerializedAs("""{"Title": "Let It Be", "Genre": 2}""", TrackContext.Default.Track,
o => o.TreatingNullAsAbsent());