Euan's Blog

C# JSON encoding with System.Text.Json - a small gotcha

Microsoft released a new namespace for working with JSON in .net core 3, called System.Text.Json. I've recently spent some time trying it out with a view to moving from Json.NET.

During this experimentation period I hit upon a small gotcha that I thought was worth documenting, as it affects quite a large amount of code that I've written.

I often find myself having common JSON properties that are shared between several different objects. I therefore define a base class with these properties. For example, I may have a BaseClass:

public class BaseClass
{
    [JsonPropertyName("int_val")] // System.Text.Json attribute to provide a property name
    [JsonProperty("int_val")] // Json.NET attribute to provide a property name
    public int IntVal { get; set; }
}

I then provide helper methods to aid with serialisation. Usually these would write to a stream or a Span<byte> or something, but in this case let's keep it simple and have it return a string — we'll provide two distinct methods for both System.Text.Json and Json.NET:

public string ToJsonSystemText()
{
    return JsonSerializer.Serialize(this, new JsonSerializerOptions
    {
        WriteIndented = false
    });
}

public string ToJsonJsonNet()
{
    return JsonConvert.SerializeObject(this, Formatting.None);
}

I would then have super classes that extend this class, adding any extra unique properties:

public class SubClass : BaseClass
{
    [JsonPropertyName("string_val")]
    [JsonProperty("string_val")]
    public string StringVal { get; set; }
}

As this SubClass inherits from BaseClass, we also have access to the ToJson methods on it. With Json.NET, calling ToJsonJsonNet produces the kind of output we'd expect (formatted here for clarity):

{
	"string_val": "hello world",
	"int_val": 42
}

However, if we call ToJsonSystemText, the JSON is subtly different:

{
	"int_val": 42
}

Any properties defined on the sub class are omitted!

The reason for this is obvious upon inspection — System.Text.Json uses a generic function when using JsonSerializer.Serialize, which ends up with the BaseClass as the generic type parameter (e.g.: when expanded the call becomes JsonSerializer.Serialize<BaseClass>(this...)); while Json.NET uses a standard function that takes an object? when using JsonConvert.SerializeObject, which ends up using the actual type of the passed object.

This subtle difference is easy to miss, and is luckily easy enough to fix — System.Text.Json has an overloaded JsonSerializer.Serialize method that takes a type as the second parameter. The altered function becomes:

public string ToJsonSystemText()
{
    return JsonSerializer.Serialize(this, GetType(), new JsonSerializerOptions()
    {
        WriteIndented = false
    });
}

I've thrown together a repository showcasing this quirk, which is available here.

#C Sharp