CSharp · Console
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization.Formatters;
using System.IO;
using System.Data;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Reflection;
using System.Collections.ObjectModel;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Text.Json.Nodes;
using NUnit.Framework; // Throws NUnit.Framework.AssertionException
#nullable enable
//https://stackoverflow.com/questions/61869393/get-net-core-jsonserializer-to-serialize-private-members
[System.AttributeUsage(System.AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class JsonIncludePrivatePropertyAttribute : System.Attribute { }
public static partial class JsonExtensions
{
public static Action<JsonTypeInfo> AddPrivateProperties<TAttribute>() where TAttribute : System.Attribute => typeInfo =>
{
if (typeInfo.Kind != JsonTypeInfoKind.Object)
return;
foreach (var type in typeInfo.Type.BaseTypesAndSelf().TakeWhile(b => b != typeof(object)))
AddPrivateProperties(typeInfo, type, p => Attribute.IsDefined(p, typeof(TAttribute)));
};
public static Action<JsonTypeInfo> AddPrivateProperties(Type declaredType) => typeInfo =>
AddPrivateProperties(typeInfo, declaredType, p => true);
public static Action<JsonTypeInfo> AddPrivateProperty(Type declaredType, string propertyName) => typeInfo =>
{
if (typeInfo.Kind != JsonTypeInfoKind.Object || !declaredType.IsAssignableFrom(typeInfo.Type))
return;
var propertyInfo = declaredType.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.NonPublic);
if (propertyInfo == null)
throw new ArgumentException(string.Format("Private roperty {0} not found in type {1}", propertyName, declaredType));
if (typeInfo.Properties.Any(p => p.GetMemberInfo() == propertyInfo))
return;
AddProperty(typeInfo, propertyInfo);
};
static void AddPrivateProperties(JsonTypeInfo typeInfo, Type declaredType, Func<PropertyInfo, bool> filter)
{
if (typeInfo.Kind != JsonTypeInfoKind.Object || !declaredType.IsAssignableFrom(typeInfo.Type))
return;
var propertyInfos = declaredType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic);
foreach (var propertyInfo in propertyInfos.Where(p => p.GetIndexParameters().Length == 0 && filter(p)))
AddProperty(typeInfo, propertyInfo);
}
static void AddProperty(JsonTypeInfo typeInfo, PropertyInfo propertyInfo)
{
if (propertyInfo.GetIndexParameters().Length > 0)
throw new ArgumentException("Indexed properties are not supported.");
var ignore = propertyInfo.GetCustomAttribute<JsonIgnoreAttribute>();
if (ignore?.Condition == JsonIgnoreCondition.Always)
return;
var name = propertyInfo.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name
?? typeInfo.Options?.PropertyNamingPolicy?.ConvertName(propertyInfo.Name)
?? propertyInfo.Name;
var property = typeInfo.CreateJsonPropertyInfo(propertyInfo.PropertyType, name);
property.Get = CreateGetter(typeInfo.Type, propertyInfo.GetGetMethod(true));
property.Set = CreateSetter(typeInfo.Type, propertyInfo.GetSetMethod(true));
property.AttributeProvider = propertyInfo;
property.CustomConverter = propertyInfo.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType is {} converterType
? (JsonConverter?)Activator.CreateInstance(converterType)
: null;
// TODO: handle ignore?.Condition == JsonIgnoreCondition.Never, WhenWritingDefault, or WhenWritingNull by setting property.ShouldSerialize appropriately
// TODO: handle JsonRequiredAttribute, JsonNumberHandlingAttribute
typeInfo.Properties.Add(property);
}
delegate TValue RefFunc<TObject, TValue>(ref TObject arg);
static Func<object, object?>? CreateGetter(Type type, MethodInfo? method)
{
if (method == null)
return null;
var myMethod = typeof(JsonExtensions).GetMethod(nameof(JsonExtensions.CreateGetterGeneric), BindingFlags.NonPublic | BindingFlags.Static)!;
return (Func<object, object?>)(myMethod.MakeGenericMethod(new[] { type, method.ReturnType }).Invoke(null, new[] { method })!);
}
static Func<object, object?> CreateGetterGeneric<TObject, TValue>(MethodInfo method)
{
if (method == null)
throw new ArgumentNullException();
if(typeof(TObject).IsValueType)
{
// https://stackoverflow.com/questions/4326736/how-can-i-create-an-open-delegate-from-a-structs-instance-method
// https://stackoverflow.com/questions/1212346/uncurrying-an-instance-method-in-net/1212396#1212396
var func = (RefFunc<TObject, TValue>)Delegate.CreateDelegate(typeof(RefFunc<TObject, TValue>), null, method);
return (o) => {var tObj = (TObject)o; return func(ref tObj); };
}
else
{
var func = (Func<TObject, TValue>)Delegate.CreateDelegate(typeof(Func<TObject, TValue>), method);
return (o) => func((TObject)o);
}
}
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 MemberInfo? GetMemberInfo(this JsonPropertyInfo property) => (property.AttributeProvider as MemberInfo);
static IEnumerable<Type> BaseTypesAndSelf(this Type? type)
{
while (type != null)
{
yield return type;
type = type.BaseType;
}
}
}
public static partial class JsonExtensions
{
public static Action<JsonTypeInfo> AddPrivateProperties() => typeInfo =>
{
if (typeInfo.Kind != JsonTypeInfoKind.Object)
return;
foreach (var type in typeInfo.Type.BaseTypesAndSelf().TakeWhile(b => b != typeof(object)))
AddPrivateProperties(typeInfo, type, p => true);
};
}
[Flags]
public enum FlagLongEnum : long
{
FlagZero = ((long)1 << 0),
FlagOne = ((long)1 << 1),
FlagTwo = ((long)1 << 2),
FlagSixtyTwo = ((long)1 << 62),
FlagSixtyThree = ((long)1 << 63),
};
public partial class Model
{
[JsonIncludePrivateProperty]
List<int> PrivateList { get; set; } = new();
[JsonIgnore] // For testing purposes only
public List<int> SurrogateList { get => PrivateList; set => PrivateList = value; }
}
public class DerivedModel : Model
{
[JsonPropertyName("MyString")]
string? PrivateString { get; set; }
int PrivateInt { get; set; }
int? PrivateNullableInt { get; set; }
[JsonConverter(typeof(JsonStringEnumConverter))]
FlagLongEnum PrivateEnum { get; set; }
[JsonIgnore]
private int Throws { get { throw new Exception(); } set { throw new Exception(); } }
[JsonIgnore]
public (string? PrivateString, int PrivateInt, int? PrivateNullableInt, FlagLongEnum PrivateEnum) Values
{
get => (PrivateString, PrivateInt, PrivateNullableInt, PrivateEnum);
set => (this.PrivateString, this.PrivateInt, this.PrivateNullableInt, this.PrivateEnum) = (value.PrivateString, value.PrivateInt, value.PrivateNullableInt, value.PrivateEnum);
}
}
public class DerivedOfDerivedModel : DerivedModel { }
public class DuplicateNameBase
{
private string? Property { get; set; }
}
public class DuplicateNameDerived : DuplicateNameBase
{
private string? Property { get; set; }
}
public class DuplicateNameDerived2 : DuplicateNameBase
{
[JsonPropertyName("SomeAlternateName")]
private string? Property { get; set; }
}
public interface IBar
{
string BarString { get; set; }
}
public class Bar : IBar
{
string IBar.BarString { get; set; } = "hello";
}
public struct BarStruct
{
public BarStruct() { }
public BarStruct(string barString) { this.BarString = barString; }
string BarString { get; set; } = "hello";
public override string ToString() => $"BarStruct.BarString = {BarString}";
}
class TestClass
{
public static void Test()
{
TestAttribute();
TestPropertyName();
TestAllProperties();
TestDuplicateName();
TestInheritance<DerivedModel>();
TestInheritance<DerivedOfDerivedModel>();
TestStructAll();
TestAll();
TestExplicit();
}
static void TestStructAll()
{
Console.WriteLine("Testing round-trip of struct {0}:", typeof(Nullable<BarStruct>));
var options = new JsonSerializerOptions
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { JsonExtensions.AddPrivateProperties(typeof(BarStruct)) },
},
};
var defaultJson = JsonSerializer.Serialize(new BarStruct(), options);
Console.WriteLine(defaultJson);
Assert.That(defaultJson.Contains(nameof(IBar.BarString)));
Nullable<BarStruct> bar = new BarStruct("foo");
var json = JsonSerializer.Serialize(bar, options);
var bar2 = JsonSerializer.Deserialize<Nullable<BarStruct>>(json, options);
Assert.AreEqual(bar.ToString(), bar2.ToString());
Console.WriteLine(JsonSerializer.Serialize(default(Nullable<BarStruct>), options));
Console.WriteLine("Round-trips of struct {0} passed.\n", typeof(Nullable<BarStruct>));
}
static void TestAll()
{
Console.WriteLine("Testing round-trip of {0} serializing all private properties:", typeof(DerivedOfDerivedModel));
var model = new DerivedOfDerivedModel
{
Values = ("hello", 101, (int?)null, FlagLongEnum.FlagOne | FlagLongEnum.FlagSixtyThree),
SurrogateList = { 1, 2, 3 },
};
var options = new JsonSerializerOptions
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { JsonExtensions.AddPrivateProperties() },
},
// Others as required
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
};
var json = JsonSerializer.Serialize(model, options);
Console.WriteLine(json);
var model2 = JsonSerializer.Deserialize<DerivedOfDerivedModel>(json, options);
Assert.AreEqual(model.Values, model2!.Values);
Assert.That(model.SurrogateList.SequenceEqual(model2!.SurrogateList));
var json2 = JsonSerializer.Serialize(model2, options);
Assert.AreEqual(json, json2);
Console.WriteLine("Round-trip successful.\n");
}
static void TestExplicit()
{
Console.WriteLine("Testing serialization of explicitly implemented properties:");
var options = new JsonSerializerOptions
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { JsonExtensions.AddPrivateProperties(typeof(Bar)) },
},
};
var json = JsonSerializer.Serialize(new Bar(), options);
Console.WriteLine(json);
Assert.That(json.Contains("IBar.BarString"));
}
static void TestDuplicateName()
{
Console.WriteLine("Testing duplicate private property names in an inheritance hierarchy:");
var options = new JsonSerializerOptions
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { JsonExtensions.AddPrivateProperties(typeof(DuplicateNameBase)),
JsonExtensions.AddPrivateProperties(typeof(DuplicateNameDerived)),
JsonExtensions.AddPrivateProperties(typeof(DuplicateNameDerived2)) },
},
};
Assert.Throws<InvalidOperationException>(() => JsonSerializer.Serialize(new DuplicateNameDerived(), options));
Assert.DoesNotThrow(() => Console.WriteLine(JsonSerializer.Serialize(new DuplicateNameDerived2(), options)));
Console.WriteLine("Passed.\n");
}
static void TestAttribute()
{
Console.WriteLine("Testing round-trop of {0} using attribute JsonIncludePrivatePropertyAttribute:", typeof(Model));
var model = new Model
{
SurrogateList = { 1, 2, 3 },
};
var options = new JsonSerializerOptions
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { JsonExtensions.AddPrivateProperties<JsonIncludePrivatePropertyAttribute>() },
},
};
var json = JsonSerializer.Serialize(model, options);
Console.WriteLine(json);
var model2 = JsonSerializer.Deserialize<Model>(json, options);
Assert.That(model.SurrogateList.SequenceEqual(model2!.SurrogateList));
var json2 = JsonSerializer.Serialize(model2, options);
Assert.AreEqual(json, json2);
Console.WriteLine("Round-trip successful.\n");
}
static void TestAllProperties()
{
Console.WriteLine("Testing round-trip of {0} using all private properties:", typeof(Model));
var model = new Model
{
SurrogateList = { 1, 2, 3 },
};
var options = new JsonSerializerOptions
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { JsonExtensions.AddPrivateProperties(typeof(Model)) },
},
};
var json = JsonSerializer.Serialize(model, options);
Console.WriteLine(json);
var model2 = JsonSerializer.Deserialize<Model>(json, options);
Assert.That(model.SurrogateList.SequenceEqual(model2!.SurrogateList));
var json2 = JsonSerializer.Serialize(model2, options);
Assert.AreEqual(json, json2);
Console.WriteLine("Round-trip successful.\n");
}
static void TestPropertyName()
{
Console.WriteLine("Testing round-trop of {0} using property name PrivateList:", typeof(Model));
var model = new Model
{
SurrogateList = { 1, 2, 3 },
};
var options = new JsonSerializerOptions
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { JsonExtensions.AddPrivateProperty(typeof(Model), "PrivateList") },
},
};
var json = JsonSerializer.Serialize(model, options);
Console.WriteLine(json);
var model2 = JsonSerializer.Deserialize<Model>(json, options);
Assert.That(model.SurrogateList.SequenceEqual(model2!.SurrogateList));
var json2 = JsonSerializer.Serialize(model2, options);
Assert.AreEqual(json, json2);
Console.WriteLine("Round-trip successful.\n");
}
static void TestInheritance<TModel>() where TModel : DerivedModel, new()
{
Console.WriteLine("Testing round-trop of {0}:", typeof(TModel));
var model = new TModel
{
Values = ("hello", 101, (int?)null, FlagLongEnum.FlagOne | FlagLongEnum.FlagSixtyThree),
SurrogateList = { 1, 2, 3 },
};
var options = new JsonSerializerOptions
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { JsonExtensions.AddPrivateProperties(typeof(DerivedModel)), JsonExtensions.AddPrivateProperties<JsonIncludePrivatePropertyAttribute>() },
},
// Others as required
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
};
var json = JsonSerializer.Serialize(model, options);
Console.WriteLine(json);
var model2 = JsonSerializer.Deserialize<TModel>(json, options);
Assert.AreEqual(model.Values, model2!.Values);
Assert.That(model.SurrogateList.SequenceEqual(model2!.SurrogateList));
var json2 = JsonSerializer.Serialize(model2, options);
Assert.AreEqual(json, json2);
Console.WriteLine("Round-trip successful.\n");
}
}
public class Program
{
public static void Main()
{
Console.WriteLine("Environment version: {0} ({1}), {2}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , Environment.Version, Environment.OSVersion);
Console.WriteLine("System.Text.Json version: " + typeof(JsonSerializer).Assembly.FullName);
Console.WriteLine();
try
{
TestClass.Test();
}
catch (Exception ex)
{
Console.WriteLine("Failed with unhandled exception: ");
Console.WriteLine(ex);
throw;
}
}
}