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;}
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:
The JsonTypeInfo
modifier approach works automatically with reference preservation and built-in support for polymorphism.
For comparison, if you were to try to implement the same functionality using a JsonConverter
, you would need to support reference preservation and polymorphism manually inside your converter's Read()
and Write()
methods.
The approach probably will not work when using source generation in a Native AOT app. The calls to MethodInfo.MakeGenericMethod()
and Delegate.CreateDelegate()
will likely fail.
See Get .NET Core JsonSerializer to serialize private members for a related problem.
Demo fiddle here