I normally use ShouldSerialize
to exclude properties that have no data such as array but now, it does not appear to be triggered when I'm only using JSON serializer in .NET Core 3
. It was being triggered when using NewtonSoft
but I've removed it from my project since it no longer appears to be required.
For example:
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;
}
Any ideas why ShouldSerializeUsers is not being triggered?
I've seen other answers where you can use:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddJsonOptions(options => {
options.SerializerSettings.NullValueHandling =
NullValueHandling.Ignore;
});
}
But I'd like to know if there is another way to handle this as I'm not using .AddMvc
Thanks.
The reason that your ShouldSerialize()
method is not triggered in ASP.NET Core 3.0 is that, in this and subsequent versions of ASP.NET, a different JSON serializer is being used by default, namely System.Text.Json.JsonSerializer
. See:
Unlike Newtonsoft, this serializer does not support the ShouldSerializeXXX()
conditional serialization pattern out of the box, see
So, what are your options for a workaround that restores this functionality?
Firstly, in .NET 7 and later, you can use a typeInfo modifier to customize your type's contract to conditionally skip properties with a corresponding ShouldSerializeXXX()
method that returns false
.
To do this, first create the following extension methods and typeInfo
modifiers:
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>() =>
static typeInfo =>
{
if (typeInfo.Kind != JsonTypeInfoKind.Object)
return;
if (typeInfo.Type == typeof(TBaseType))
DoAddShouldSerializeMethodsForType<TBaseType>(typeInfo);
else if (typeInfo.Type.IsAssignableTo(typeof(TBaseType)))
{
// MakeGenericMethod() might not work in AOT scenarios
typeof(JsonExtensions)
.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))
return;
foreach (var propertyInfo in typeInfo.Properties)
{
if (!(propertyInfo.GetMemberInfo() is {} memberInfo))
continue;
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)
{
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);
}
}
}
Then in AddJsonOptions()
you could enable checking for ShouldSerializeXXX()
methods for a particular ModelDto
type as follows:
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.TypeInfoResolver =
(options.JsonSerializerOptions.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver())
.WithAddedModifier(JsonExtensions.AddShouldSerializeMethodsForType<ModelDto>());
// Set any additional options here
});
Or if you would prefer to enable ShouldSerializeXX()
methods for all types, use JsonExtensions.AddShouldSerializeMethodsForTypeHierarchy<object>()
:
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.TypeInfoResolver =
(options.JsonSerializerOptions.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver())
.WithAddedModifier(JsonExtensions.AddShouldSerializeMethodsForTypeHierarchy<object>());
// Set any additional options here
});
Notes:
Not tested with Native AOT.
JsonExtensions.AddShouldSerializeMethodsForTypeHierarchy<object>()
probably won't work with Native AOT, you will need to enable conditional serialization methods for each specific type.
Demo fiddle #1 here.
Secondly, in .NET 5 and later, as indicated in this answer by Ilya Chernomordik, you could create some nullable surrogate property that returns a null
value when it should not be serialized, and mark the property with [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
.
Finally, you can revert back to using Newtonsoft as shown in this answer to Where did IMvcBuilder AddJsonOptions go in .Net Core 3.0? by poke, and also Add Newtonsoft.Json-based JSON format support:
Then call AddNewtonsoftJson()
in Startup.ConfigureServices
:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddNewtonsoftJson();
}