[Fork] [Fork] [Fork] https://stackoverflow.com/questions/61869393/get-net-core-jsonserializer-to-serialize-private-members

CSharp · Console

/Program.cs

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;
			}
		}
	}
;