Migrating C# from Newtonsoft.Json to System.Text.Json for .NET 5

This blog post contains my notes in migrating a small C# .NET codebase from Newtonsoft.Json to System.Text.Json (STJ). First, let me thank Newtonsoft, which has close to a billion dowloads, for making JSON processing possible before Microsoft and for shaping STJ. There are plenty of existing resources about this, but to help me remember, these notes address the patterns I seem to use most often. I had put this effort off for some time, but it turned out to be relatively painless (an hour or two, with another hour or two taking these notes). I will not claim this is best practice or defect free, but it seems to work.

Update 18.May.2021: Actually, something is not working, specifically deserialization to structures in lists. I have tried a few things, but have not made progress. Changing one line of code to deserialize with Newtonsoft addresses the issue, but I am hesitant to revert to Newtonsoft, so I am stuck. Update 27.May.2021: I no longer have this issue after implementing the following.

Update 6.Dec.2021: It looks like you need .NET 6 to update the JSON in memory.

End of Updates.

In the past, I think it was best to use Newtonsoft.Json to process JSON with .NET. Since .NET Core 3 and especially since .NET 5, it may be better to use System.Text.Json, if just to avoid the need to reference the Newtonsoft packages from .NET projects. In addition to being ubiquitous with .NET, I think that I read that STJ is faster than Newtonsoft. Consumers that prefer Newtonsoft can always get raw JSON from STJ, and I think I even saw an option to get STJ to use Newtonsoft internally.

Start by backing up the projects, removing the Newtonsoft.Json package reference from them, and replacing either or both of these two lines.

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

With this one.

using System.Text.Json;

I have used JObject extensively, which is the wrong name for any class. There may be cases where it is preferable to replace JObject with JsonDocument, but in general I prefer to replace it with JsonElement, which you have to get from a JsonDocument. Here is some Newtonsoft.

JObject jObject = JObject.Parse(jsonString);

Corresponding STJ:

JsonElement jsonElement = JsonDocument.Parse(jsonString).RootElement;

To get the raw JSON back as a string, JObject.ToString() becomes JsonElement.GetRawText().

Where you do not need the JsonElement, you can use JsonSerializer.Deserialize() instead.

TType instance = JsonSerializer.Deserialize<TType>(jsonString);

It seems you need to use an intermediary to serialize from a single element in the JSON.

JsonSerializer.Deserialize<TType>(
     JsonDocument.Parse(jsonString).RootElement.GetProperty(
        "arrayName")[arrayIndex].GetRawText(),
     new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });

Replace SelectToken() JsonPath statements with methods that access the JSON structure.

jObject.SelectToken("$.entry");

The preceding Newtonsoft becomes the following STJ.

jsonElement.GetProperty("entry");

Here is another Newtonsoft example.

foreach (JObject contentType in jObject.SelectToken("$.content_types").Children())
{
    if (contentType.SelectToken(".schema[?(@uid=='url')]") != null)

There is probably a better way than this replacement for STJ.

foreach (JsonElement contentType in page.GetProperty("content_types").EnumerateArray())
{
    foreach (JsonElement field in contentType.GetProperty("schema").EnumerateArray())
    {
        if (field.GetProperty("uid").ToString() == "url")
        {
            include = true;
            break;

You also get the values differently than in Newtonsoft.

contentType["uid"].ToString()

Here is the corresponding STJ.

contentType.GetProperty("uid").ToString()

It appears that [JsonProperty(“file_size”)] becomes [JsonPropertyName(“file_size”)].

Supposedly for performance, System.Text.Json deserialization is case-sensitive in matching property names, where Newtonsoft is not. *IF YOUR OBJECT PROPERTIES ARE EMPTY AFTER DESERIALIZATION, YOU PROBABLY FORGOT ABOUT THIS*. Miraculously, there does not seem to be a way to set this case sensitivity or any options globally. Maybe it is best to be explicit everywhere case-insensitivity is needed, which is everywhere. So Newtonsoft JsonConvert.DefaultSettings disappears and you get to do things like this instead.

TType instance = JsonSerializer.Deserialize<TType>(
    jsonElement,
    new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });

The biggest challenge I faced involved JsonConverters. I do not know how to explain it, so here is the Newtonsoft code.

public override TBlockBase ReadJson(
    JsonReader reader,
    Type objectType,
    TBlockBase existingValue,
    bool hasExistingValue,
    JsonSerializer serializer)
{
    TBlockTypeEnum parsed;
    JObject jObject = JObject.Load(reader);

    if (Enum.TryParse(
        jObject.Children().First().Children().First().Path,
        true,
        out parsed))
    {
        foreach (Type t in Assembly.GetAssembly(
            typeof(TBlockBase)).GetTypes().Where(
                myType => myType.IsClass
                    && !myType.IsAbstract
                    && myType.IsSubclassOf(typeof(TBlockBase))))
        {
            TBlockBase obj = (TBlockBase)Activator.CreateInstance(t);

            foreach (PropertyInfo propertyInfo
                in obj.GetType().GetRuntimeProperties())
            {
                if (propertyInfo.PropertyType == typeof(TBlockTypeEnum))
                {
                    if (((int)propertyInfo.GetValue(obj))
                        == parsed.ToInt32(CultureInfo.InvariantCulture))
                    {
                        serializer.Populate(jObject.GetValue(
                            parsed.ToString().ToLower()).CreateReader(),
                            obj);
                        return obj;

Here is the corresponding STJ, which requires a using directive for System.Text.Json.Serialization.

using System.Text.Json.Serialization;

public override TBlockBase Read(
  ref Utf8JsonReader reader, 
  Type typeToConvert, 
  JsonSerializerOptions options)
{
    // move from the StartObject token to first element, which is the type of the modular block
    reader.Read();

    // block type
    TBlockTypeEnum parsed;

    if (Enum.TryParse(
        reader.GetString(),
        true,
        out parsed))
    {
        foreach (Type t in Assembly.GetAssembly(
            typeof(TBlockBase)).GetTypes().Where(
                myType => myType.IsClass
                    && !myType.IsAbstract
                    && myType.IsSubclassOf(typeof(TBlockBase))))
        {
            TBlockBase obj = (TBlockBase)Activator.CreateInstance(t);

            foreach (PropertyInfo propertyInfo
                in obj.GetType().GetRuntimeProperties())
            {
                if (propertyInfo.PropertyType == typeof(TBlockTypeEnum))
                {
                    if (((int)propertyInfo.GetValue(obj))
                        == parsed.ToInt32(CultureInfo.InvariantCulture))
                    {
                        TBlockBase result = (TBlockBase)JsonSerializer.Deserialize(
                            ref reader, obj.GetType()); //TODO probably should pass options here?

                        // move parser past the close of the StartObject
                        reader.Read();

                        return result;

Again, without global settings, I need to be explicit where I use these (thank goodness for generics, but actually I still have this code in just one method). *IF YOUR OBJECT PROPERTIES ARE EMPTY AFTER DESERIALIZATION, YOU PROBABLY FORGOT ABOUT THIS.* Here is explicitly registering the converters.

JsonSerializerOptions options = new JsonSerializerOptions()
    { PropertyNameCaseInsensitive = true };

foreach (Type blockType in ModularBlockBase.Implementations)
{
    MethodInfo methodInfo = blockType.GetRuntimeMethod(
        "GetJsonConverter", new Type[0]);
    object obj = Activator.CreateInstance(blockType);
    JsonConverter jsonConverter = methodInfo.Invoke(
        obj, new object[0]) as JsonConverter;
    options.Converters.Add(jsonConverter);
}

JsonElement json = 
    JsonDocument.Parse(AsStringAsync(entryId).Result).RootElement.GetProperty("entry");
object entryModel = JsonSerializer.Deserialize(json.GetRawText(),
    ContentTypeIdentifierAttribute.GetModelTypeForContentType(entryId.ContentType), 
    options);

Update 17.May.2021: This may explain how to set global JSON serialization options.

services.ConfigureOptions<ConfigureJsonOptions>();

...

private class ConfigureJsonOptions : IConfigureOptions<JsonOptions>
{
    private readonly IFooService _fooService;

    public ConfigureJsonOptions(IFooService fooService)
    {
        _fooService = fooService;
    }

    public void Configure(JsonOptions options)
    {
        options.JsonSerializerOptions.Converters
            .Add(new FooConverter(_fooService));
    }
}

2 thoughts on “Migrating C# from Newtonsoft.Json to System.Text.Json for .NET 5

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: