Search code examples
c#json.netdeserializationsystem.text.json

System.Text.Json Custom Converter to enable deserialisation of private setter properties


I need to serialize and deserialize a lot of different classes in my project. Many of there classes have properies with private or internal setter, but for me it is important to deserialize these properties too.

Using a parameterised constuctor is not an option, because I need to keep references during the Json-roundtrip.

Adding [JsonInclude] would work, but then I have to add this to hundreds of properties.

So I tried to write a Custom Converter that just do the default deserialization, exept that all setters of properies are used. But all of my versions end up in some error, once the references are not kept during deserialization, sometimes the recursive converter calls lead to stack overflow exception....

Have anyone created something similar? Or is there an easy way to do this?

UPDATE: I am using .net8, and no code source generation is planed.

Here is my current version of the converter:

public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options){


var newOptions = new JsonSerializerOptions(options);

newOptions.Converters.Clear();

Dictionary<string, object>? dict = JsonSerializer.Deserialize<Dictionary<string, object>?>(ref reader, newOptions);

if (dict != null)
{
    T? obj = (T?)Activator.CreateInstance(typeof(T), true);

    foreach (var prop in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
    {
        if (dict.TryGetValue(prop.Name, out var value))
            if (prop.CanWrite)
            {
                object? convertedValue;

                if (value is JsonElement jsonElement)
                {
                    if (prop.PropertyType == typeof(int))
                    {
                        convertedValue = jsonElement.GetInt32(); 
                    }
                    else if (prop.PropertyType == typeof(string))
                    {
                        convertedValue = jsonElement.GetString();
                    }
                    else if (prop.PropertyType == typeof(double))
                    {
                        convertedValue = jsonElement.GetDouble();
                    }
                    else if (prop.PropertyType == typeof(bool))
                    {
                        convertedValue = jsonElement.GetBoolean();
                    }
                    else
                    {
                        convertedValue = jsonElement.Deserialize(prop.PropertyType, options);
                    }
                }
                else
                {
                    convertedValue = Convert.ChangeType(value, prop.PropertyType);
                }
                prop.SetValue(obj, convertedValue);

            }
    }
    return obj;
}
return default;}

Solution

  • Rather than a converter, you can use a custom JsonTypeInfo modifier to add calls to private setters for all serialized properties.

    First add the following extension methods:

    public static partial class JsonExtensions
    {
        public static void DeserializePrivateSetters(JsonTypeInfo typeInfo)
        {
            if (typeInfo.Kind != JsonTypeInfoKind.Object)
                return;
            // Add a Set function for all properties that have a Get function and an underlying set MethodInfo even if private
            foreach (var property in typeInfo.Properties)
                if (property.Get != null && property.Set == null 
                    && property.GetPropertyInfo() is {} info 
                    && info.GetSetMethod(true) is {} setMethod)
                {
                    property.Set = CreateSetter(typeInfo.Type, setMethod);
                }
        }
    
        static Action<object,object?>? CreateSetter(Type type, MethodInfo? method)
        {
            if (method == null)
                return null;
            var myMethod = typeof(JsonExtensions).GetMethod(nameof(JsonExtensions.CreateSetterGeneric), BindingFlags.NonPublic | BindingFlags.Static)!;
            return (Action<object,object?>)(myMethod.MakeGenericMethod(new [] { type, method.GetParameters().Single().ParameterType }).Invoke(null, new[] { method })!);
        }
        
        static Action<object,object?>? CreateSetterGeneric<TObject, TValue>(MethodInfo method)
        {
            if (method == null)
                throw new ArgumentNullException();
            if (typeof(TObject).IsValueType)
            {
                // TODO: find a performant way to do this.  Possibilities:
                // Box<T> from Microsoft.Toolkit.HighPerformance
                // https://stackoverflow.com/questions/18937935/how-to-mutate-a-boxed-struct-using-il
                return (o, v) => method.Invoke(o, new [] { v });
            }
            else
            {
                var func = (Action<TObject, TValue?>)Delegate.CreateDelegate(typeof(Action<TObject, TValue?>), method);
                return (o, v) => func((TObject)o, (TValue?)v);
            }
        }
    
        static PropertyInfo? GetPropertyInfo(this JsonPropertyInfo property) => (property.AttributeProvider as PropertyInfo);
    }
    

    Then set up your options as follows:

    var options = new JsonSerializerOptions
    {
        TypeInfoResolver = new DefaultJsonTypeInfoResolver()
            .WithAddedModifier(JsonExtensions.DeserializePrivateSetters),
        // Add any other options as needed.  E.g. you mentioned using reference preservation.
        ReferenceHandler = ReferenceHandler.Preserve,
        WriteIndented = true,
    };
    

    And now JsonPropertyInfo.Set callbacks will be added to the JsonTypeInfo contract for all types that are serialized as JSON objects and have serialized properties with private or internal setters.

    Notes:

    Demo fiddle here