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;
public static partial class JsonExtensions
const string ShouldSerializePrefix = "ShouldSerialize";
public static MemberInfo? GetMemberInfo(this JsonPropertyInfo property) => property.AttributeProvider as MemberInfo;
public static Action<JsonTypeInfo> AddShouldSerializeMethodsForTypeHierarchy<TBaseType>() =>
if (typeInfo.Kind != JsonTypeInfoKind.Object)
if (typeInfo.Type == typeof(TBaseType))
DoAddShouldSerializeMethodsForType<TBaseType>(typeInfo);
else if (typeInfo.Type.IsAssignableTo(typeof(TBaseType)))
.GetMethod(nameof(JsonExtensions.DoAddShouldSerializeMethodsForType), BindingFlags.NonPublic | BindingFlags.Static)!
.MakeGenericMethod(new [] { typeInfo.Type })
.Invoke(null, new [] { typeInfo } );
public static Action<JsonTypeInfo> AddShouldSerializeMethodsForType<TType>() =>
static typeInfo => DoAddShouldSerializeMethodsForType<TType>(typeInfo);
static void DoAddShouldSerializeMethodsForType<TType>(JsonTypeInfo typeInfo)
if (typeInfo.Kind != JsonTypeInfoKind.Object || typeInfo.Type != typeof(TType))
foreach (var propertyInfo in typeInfo.Properties)
if (!(propertyInfo.GetMemberInfo() is {} memberInfo))
var method = memberInfo.DeclaringType?.GetMethod(ShouldSerializePrefix + memberInfo.Name, Array.Empty<Type>());
if (method != null && method.ReturnType == typeof(bool))
var originalShouldSerialize = propertyInfo.ShouldSerialize;
var shouldSerializeMethod = CreateTypedDelegate<TType, bool>(method);
propertyInfo.ShouldSerialize =
originalShouldSerialize == null
? (obj, value) => shouldSerializeMethod(obj)
: (obj, value) => originalShouldSerialize(obj, value) && shouldSerializeMethod(obj);
delegate TValue RefFunc<TObject, TValue>(ref TObject arg);
static Func<object, TValue> CreateTypedDelegate<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);
private ICollection<UserDto>? _users;
public ICollection<UserDto> Users
get => this._users ?? (this._users = new HashSet<UserDto>());
set => this._users = value;
public bool ShouldSerializeUsers()
return this._users?.Count > 0;
public string? Name { get; set; }
public interface IHasSomeValue { int SomeValue { get; set; } }
public abstract class BaseClass : IHasSomeValue
public virtual int SomeValue { get; set; }
public abstract bool ShouldSerializeSomeValue();
public class DerivedClass : BaseClass
public override bool ShouldSerializeSomeValue() { return false; }
public class DerivedOfDerivedClass : DerivedClass
public override bool ShouldSerializeSomeValue() { return true; }
public virtual int SomeOtherValue { get; set; }
public virtual bool ShouldSerializeSomeOtherValue() => false;
public class DerivedOfDerivedOfDerivedClass : DerivedOfDerivedClass
public override int SomeOtherValue { get => base.SomeOtherValue; }
public override int SomeValue { get => base.SomeValue; }
public class DerivedOfDerivedOfDerivedOfDerivedClass : DerivedOfDerivedOfDerivedClass
public override bool ShouldSerializeSomeValue() { return !base.ShouldSerializeSomeValue(); }
public override bool ShouldSerializeSomeOtherValue() { return !base.ShouldSerializeSomeOtherValue(); }
public struct MyStruct : IHasSomeValue
public int SomeValue { get; set; }
public bool ShouldSerializeSomeValue() => false;
public static void Test()
TestSerializeTypeAndHierarchy<object, ModelDto>("{}", new());
TestSerializeSomeValueTypeAndHierarchy<DerivedClass>("""{}""", 21);
TestSerializeSomeValueTypeAndHierarchy<DerivedOfDerivedClass>("""{"SomeValue":21}""", 21);
TestSerializeSomeValueTypeAndHierarchy<DerivedOfDerivedOfDerivedClass>("""{"SomeValue":21}""", 21);
TestSerializeSomeValueTypeAndHierarchy<DerivedOfDerivedOfDerivedOfDerivedClass>("""{"SomeOtherValue":0}""", 21);
TestSerializeSomeValueType<MyStruct>("""{}""", 21);
TestSerializeSomeValueTypeHierarchy<object, MyStruct>("""{}""", 21);
static void TestSerializeTypeAndHierarchy<TBaseType, TDerivedType>(string expectedJson, TDerivedType model)
TestSerializeType<TDerivedType>(expectedJson, model);
TestSerializeTypeHierarchy<TBaseType, TDerivedType>(expectedJson, model);
static void TestSerializeType<ModelDto>(string expectedJson, ModelDto model)
var options = new { JsonSerializerOptions = new JsonSerializerOptions() };
options.JsonSerializerOptions.TypeInfoResolver =
(options.JsonSerializerOptions.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver())
.WithAddedModifier(JsonExtensions.AddShouldSerializeMethodsForType<ModelDto>());
var json = JsonSerializer.Serialize(model, options.JsonSerializerOptions);
Console.WriteLine("For type {0}, JSON = {1}", model!.GetType(), json);
Assert.That(JsonNode.DeepEquals(JsonNode.Parse(json), JsonNode.Parse(expectedJson)));
static void TestSerializeTypeHierarchy<TBaseType, TDerivedType>(string expectedJson, TDerivedType model)
var options = new { JsonSerializerOptions = new JsonSerializerOptions() };
options.JsonSerializerOptions.TypeInfoResolver =
(options.JsonSerializerOptions.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver())
.WithAddedModifier(JsonExtensions.AddShouldSerializeMethodsForTypeHierarchy<ModelDto>());
var json = JsonSerializer.Serialize(model, options.JsonSerializerOptions);
Console.WriteLine("For type {0} (base type {1}), JSON = {2}", model!.GetType(), typeof(TBaseType), json);
Assert.That(JsonNode.DeepEquals(JsonNode.Parse(json), JsonNode.Parse(expectedJson)));
static void TestSerializeSomeValueType<TType>(string expectedJson, int someValue = 21) where TType : IHasSomeValue, new()
TType model = new TType { SomeValue = someValue };
var options = new JsonSerializerOptions
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
.WithAddedModifier(JsonExtensions.AddShouldSerializeMethodsForType<TType>()),
var json = JsonSerializer.Serialize(model, options);
Console.WriteLine("For type {0}, JSON = {1}", model.GetType(), json);
Assert.That(JsonNode.DeepEquals(JsonNode.Parse(json), JsonNode.Parse(expectedJson)));
static void TestSerializeSomeValueTypeAndHierarchy<TType>(string expectedJson, int someValue = 21) where TType : BaseClass, new()
TestSerializeSomeValueType<TType>(expectedJson, someValue);
TestSerializeSomeValueTypeHierarchy<TType, TType>(expectedJson, someValue);
TestSerializeSomeValueTypeHierarchy<BaseClass, TType>(expectedJson, someValue);
TestSerializeSomeValueTypeHierarchy<object, TType>(expectedJson, someValue);
static void TestSerializeSomeValueTypeHierarchy<TBaseType, TDerivedType>(string expectedJson, int someValue = 21) where TDerivedType : IHasSomeValue, TBaseType, new()
TDerivedType model = new TDerivedType { SomeValue = someValue };
var options = new JsonSerializerOptions
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
.WithAddedModifier(JsonExtensions.AddShouldSerializeMethodsForTypeHierarchy<TBaseType>()),
var json = JsonSerializer.Serialize(model, options);
Console.WriteLine("For type {0} (base type {1}), JSON = {2}", model.GetType(), typeof(TBaseType), json);
Assert.That(JsonNode.DeepEquals(JsonNode.Parse(json), JsonNode.Parse(expectedJson)));
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: ");
public static partial class JsonExtensions
static PropertyInfo? GetProperty(this MethodInfo method)
bool hasReturn = method.ReturnType != typeof(void);
var properties = method.DeclaringType?.GetProperties(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
return properties?.Where(prop => prop.GetSetMethod() == method).FirstOrDefault();
return properties?.Where(prop => prop.GetGetMethod() == method).FirstOrDefault();
public static PropertyInfo GetBaseDefinition(this PropertyInfo info)
static PropertyInfo? GetBasePropertyDefinition(MethodInfo? method)
while (baseMethod.GetBaseDefinition() is {} m && m != baseMethod)
return baseMethod != method ? baseMethod.GetProperty() : null;
if (GetBasePropertyDefinition(info.GetGetMethod()) is {} p1)
if (GetBasePropertyDefinition(info.GetSetMethod()) is {} p2)
public static MemberInfo GetBaseDefinition(this MemberInfo info) =>
MethodInfo m => m.GetBaseDefinition(),
PropertyInfo p => p.GetBaseDefinition(),
_ => throw new NotImplementedException(info.GetType().Name),
public static string? GetMemberName(this JsonPropertyInfo property) => property.GetMemberInfo()?.Name;
public static partial class JsonExtensions
public static ref Utf8JsonReader ReadAndAssert(ref this Utf8JsonReader reader) { if (!reader.Read()) { throw new JsonException(); } return ref reader; }
public static T ThrowOnNull<T>(this T? value) where T : class => value ?? throw new ArgumentNullException();
public static void TestReflection()
var type = typeof(DerivedOfDerivedClass);
var method = type.GetMethod(nameof(BaseClass.ShouldSerializeSomeValue))!;
Console.WriteLine("{0}: {1}", method, method.DeclaringType);
Console.WriteLine("{0}: {1}", method.GetBaseDefinition(), method.GetBaseDefinition().DeclaringType);
var property = type.GetProperty(nameof(BaseClass.SomeValue))!;
Console.WriteLine("{0}: {1}", property, property.DeclaringType);