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.
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.