I have models that include observable properties generated automatically from [ObservableProperty]
fields via the CommunityToolkit.Mvvm
toolkit, and I would like to serialize them using a System.Text.Json JsonSerializerContext
. But when I do, all my properties are missing. How can I fix this?
Here is my code:
using CommunityToolkit.Mvvm.ComponentModel;
using System.IO.Ports;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Unicode;
namespace WinFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
var _result = JsonSerializer.Serialize<EmptyConnection>(new EthernetConnection() { IP="192.168.125.201",Port=9100}, new JsonSerializerOptions()
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.Create(UnicodeRanges.All),
TypeInfoResolver = EmptyConnectionGenerationContext.Default,
IgnoreReadOnlyProperties = true,
});
}
[JsonDerivedType(typeof(EmptyConnection), typeDiscriminator: "EmptyConnection")]
[JsonDerivedType(typeof(SerialPortConnection), typeDiscriminator: "SerialPortConnection")]
[JsonDerivedType(typeof(EthernetConnection), typeDiscriminator: "EthernetConnection")]
public partial class EmptyConnection : ObservableObject
{
}
public partial class SerialPortConnection : EmptyConnection
{
[ObservableProperty]
string _PortName = "";
[ObservableProperty]
int? _BaudRate = null;
[ObservableProperty]
Parity? _Parity = null;
[ObservableProperty]
int? _DataBits = null;
[ObservableProperty]
StopBits? _StopBits = null;
[ObservableProperty]
string _PortFeature = "";
}
public partial class EthernetConnection : EmptyConnection
{
[ObservableProperty]
string _IP = "";
[ObservableProperty]
int _Port = 0;
}
[JsonSerializable(typeof(EmptyConnection))]
[JsonSerializable(typeof(SerialPortConnection))]
[JsonSerializable(typeof(EthernetConnection))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(int?))]
[JsonSerializable(typeof(Parity?))]
[JsonSerializable(typeof(StopBits?))]
public partial class EmptyConnectionGenerationContext : JsonSerializerContext
{
}
}
}
After the program ran, it generates an empty Json: {"$type":"EthernetConnection"}
If I don't use the TypeInfoResolver
in JsonSerializerOptions
,it will generate Json successfully like this:
{"$type":"EthernetConnection","IP":"192.168.125.201","Port":9100}
What's wrong with my code?
What you are experiencing is an interaction bug between two technologies that use source generators:
JsonSerializerContext
uses source generators to generate serializers for specified types at compile time.CommunityToolkit.Mvvm
's [ObservableProperty]
uses source generators to generate an observable property for the specified field.You are hoping that these two source generators will work together so that the properties generated by CommunityToolkit.Mvvm
will be picked up by the JsonSerializerContext
and serialized, but unfortunately this seems not to be implemented. Code generated by once source generator is not visible to another source generator when building a single project. For confirmation, see:
So what are your options?
Firstly, you could manually implement the observable properties following the pattern shown in the docs, removing [ObservableProperty]
and adding in the required properties for your fields. E.g.
[ObservableProperty]
string _IP = "";
Would become
string _IP = "";
public string IP
{
get => _IP;
set => SetProperty(ref _IP, value);
}
Secondly, as you have already noticed, you could use reflection based serialization rather than source-generation based serialization:
var options = new JsonSerializerOptions()
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.Create(UnicodeRanges.All),
//TypeInfoResolver = EmptyConnectionGenerationContext.Default, // REMOVE THIS
IgnoreReadOnlyProperties = true,
};
var _result = JsonSerializer.Serialize<EmptyConnection>(new EthernetConnection() { IP="192.168.125.201", Port=9100 }, options);
Console.WriteLine(_result); // {"$type":"EthernetConnection","IP":"192.168.125.201","Port":9100}
Finally, as suggested by this answer by Steven Blom, you could extract your data model and JsonSerializerContext
into separate projects, making the serialization context project depend on the data model project. If you do that, you guarantee that the source generators for System.Text.Json will see the observable properties for your data model because the project containing your data model will already have been built. Then your WinFormsApp1
project would need to reference both projects in order to serialize your data model using a JsonSerializerContext
.