'Get .NET Core JsonSerializer to serialize private members

I have a class with a private List<T> property which I would like to serialize/deserialize using the JsonSerializer. Use of the JsonPropertyAttribute doesn't seem to be supported in .NET Core. So how can I have my private list property serialized?

I'm using System.Text.Json for this.



Solution 1:[1]

It seems System.Text.Json does not support private property serialization.

https://docs.microsoft.com/tr-tr/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to#internal-and-private-property-setters-and-getters

But as the Microsoft's document says, you can do it with custom converters.

https://www.thinktecture.com/en/asp-net/aspnet-core-3-0-custom-jsonconverter-for-the-new-system_text_json/

Code snippet for serialization;

  public class Category
    {
        public Category(List<string> names)
        {
            this.Names1 = names;
        }

        private List<string> Names1 { get; set; }
        public string Name2 { get; set; }
        public string Name3 { get; set; }
    }


 public class CategoryJsonConverter : JsonConverter<Category>
    {
        public override Category Read(ref Utf8JsonReader reader,
                                      Type typeToConvert,
                                      JsonSerializerOptions options)
        {
                       var name = reader.GetString();

            var source = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(name);

            var category = new Category(null);

            var categoryType = category.GetType();
            var categoryProps = categoryType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

            foreach (var s in source.Keys)
            {
                var categoryProp = categoryProps.FirstOrDefault(x => x.Name == s);

                if (categoryProp != null)
                {
                    var value = JsonSerializer.Deserialize(source[s].GetRawText(), categoryProp.PropertyType);

                    categoryType.InvokeMember(categoryProp.Name,
                        BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.SetProperty | BindingFlags.Instance,
                        null,
                        category,
                        new object[] { value });
                }
            }

            return category;
        }

        public override void Write(Utf8JsonWriter writer,
                                   Category value,
                                   JsonSerializerOptions options)
        {
            var props = value.GetType()
                             .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                             .ToDictionary(x => x.Name, x => x.GetValue(value));

            var ser = JsonSerializer.Serialize(props);

            writer.WriteStringValue(ser);
        }
    }

static void Main(string[] args)
    {
        Category category = new Category(new List<string>() { "1" });
        category.Name2 = "2";
        category.Name3 = "3";

        var opt = new JsonSerializerOptions
        {
            Converters = { new CategoryJsonConverter() },
            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
        };

        var json = JsonSerializer.Serialize(category, opt);

        var obj = JsonSerializer.Deserialize<Category>(json, opt);

        Console.WriteLine(json);
        Console.ReadKey();
    }

Result;

"{\"Names1\":[\"1\"],\"Name2\":\"2\",\"Name3\":\"3\"}"

Solution 2:[2]

System.Text.Json supports private property serialization starting with .NET 5 according to micosoft documentation

System.Text.Json supports private and internal property setters and getters via the [JsonInclude] attribute.Find more details here

Solution 3:[3]

Although you cannot serialize a private field directly as is, you can do it indirectly.

You need to provide a public property for the field and a constructor as in the following example:

class MyNumbers
{
    // This private field will not be serialized
    private List<int> _numbers;

    // This public property will be serialized
    public IEnumerable<int> Numbers => _numbers;

    // The serialized property will be recovered with this dedicated constructor
    // upon deserialization. Type and name must be the same as the public property.
    public MyNumbers(IEnumerable<int> Numbers = null)
    {
        _numbers = Numbers as List<int> ?? Numbers?.ToList() ?? new();
    }
}

The following code demonstrates how that works:

string json;
// Serialization
{
    MyNumbers myNumbers = new(new List<int> { 10, 20, 30});
    json = JsonSerializer.Serialize(myNumbers);
    Console.WriteLine(json);
}
// Deserialization
{
    var myNumbers2 = JsonSerializer.Deserialize<MyNumbers>(json);
    foreach (var number in myNumbers2.Numbers)
        Console.Write(number + "  ");
}

Output:

{"Numbers":[10,20,30]}
10  20  30

If you want to detract people from accessing your private data, you can change the name to something explicitly forbidden like __private_numbers.

class MyNumbers2
{
    private List<int> _numbers;

    public IEnumerable<int> __private_numbers => _numbers;

    public MyNumbers2(IEnumerable<int> __private_numbers = null)
    {
        _numbers = __private_numbers as List<int> ?? __private_numbers?.ToList() ?? new();
    }
}

If an external coder is fool enough to access that private data as if it was part of the normal programming interface of that class, then shame on him. You are in your plain right to change that "private interface" without any guilt. And he can't mess with your internal list either, with an IEnumerable.

In most situations, that should be enough.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Manzur Alahi
Solution 3 Frederic