using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Collections.ObjectModel;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Text.Json.Nodes;
[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)
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))
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))
AddProperty(typeInfo, propertyInfo);
static void AddPrivateProperties(JsonTypeInfo typeInfo, Type declaredType, Func<PropertyInfo, bool> filter)
if (typeInfo.Kind != JsonTypeInfoKind.Object || !declaredType.IsAssignableFrom(typeInfo.Type))
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)
var name = propertyInfo.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name
?? typeInfo.Options?.PropertyNamingPolicy?.ConvertName(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)
typeInfo.Properties.Add(property);
delegate TValue RefFunc<TObject, TValue>(ref TObject arg);
static Func<object, object?>? CreateGetter(Type type, MethodInfo? method)
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)
throw new ArgumentNullException();
if(typeof(TObject).IsValueType)
var func = (RefFunc<TObject, TValue>)Delegate.CreateDelegate(typeof(RefFunc<TObject, TValue>), null, method);
return (o) => {var tObj = (TObject)o; return func(ref tObj); };
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)
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)
throw new ArgumentNullException();
if (typeof(TObject).IsValueType)
return (o, v) => method.Invoke(o, new [] { v });
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)
public static partial class JsonExtensions
public static Action<JsonTypeInfo> AddPrivateProperties() => typeInfo =>
if (typeInfo.Kind != JsonTypeInfoKind.Object)
foreach (var type in typeInfo.Type.BaseTypesAndSelf().TakeWhile(b => b != typeof(object)))
AddPrivateProperties(typeInfo, type, p => true);
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();
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; }
private int Throws { get { throw new Exception(); } set { throw new Exception(); } }
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; }
string BarString { get; set; }
string IBar.BarString { get; set; } = "hello";
public BarStruct(string barString) { this.BarString = barString; }
string BarString { get; set; } = "hello";
public override string ToString() => $"BarStruct.BarString = {BarString}";
public static void Test()
TestInheritance<DerivedModel>();
TestInheritance<DerivedOfDerivedModel>();
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>));
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() },
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
var json = JsonSerializer.Serialize(model, options);
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);
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));
SurrogateList = { 1, 2, 3 },
var options = new JsonSerializerOptions
TypeInfoResolver = new DefaultJsonTypeInfoResolver
Modifiers = { JsonExtensions.AddPrivateProperties<JsonIncludePrivatePropertyAttribute>() },
var json = JsonSerializer.Serialize(model, options);
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));
SurrogateList = { 1, 2, 3 },
var options = new JsonSerializerOptions
TypeInfoResolver = new DefaultJsonTypeInfoResolver
Modifiers = { JsonExtensions.AddPrivateProperties(typeof(Model)) },
var json = JsonSerializer.Serialize(model, options);
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));
SurrogateList = { 1, 2, 3 },
var options = new JsonSerializerOptions
TypeInfoResolver = new DefaultJsonTypeInfoResolver
Modifiers = { JsonExtensions.AddPrivateProperty(typeof(Model), "PrivateList") },
var json = JsonSerializer.Serialize(model, options);
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));
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>() },
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
var json = JsonSerializer.Serialize(model, options);
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 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("Failed with unhandled exception: ");