Search code examples
c#.net-coresystem.text.jsonsourcegeneratorscommunity-toolkit-mvvm

Why does JsonSerializerContext generate empty JSON for a model containing [ObservableProperty] properties?


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?


Solution

  • What you are experiencing is an interaction bug between two technologies that use source generators:

    • System.Text.Json's JsonSerializerContext uses source generators to generate serializers for specified types at compile time.
    • When applied to a field, 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.