Override newtonsoft.json serialization in ASP.NET Core to make the typeNameHandling to be Auto only for objects and not arrays

Viewed 879

My issue is that I need some $type on my serialized json, I have some subclasses and it's very useful for me.

This is my startup

services.AddTransient<Microsoft.Extensions.Options.IConfigureOptions<Microsoft.AspNetCore.Mvc.MvcNewtonsoftJsonOptions>, App_Start.MvcJsonOptionsSetup>();
services.AddControllers()
        .AddNewtonsoftJson()
        .SetCompatibilityVersion(CompatibilityVersion.Latest)
        .AddControllersAsServices();

internal class MvcJsonOptionsSetup : IConfigureOptions<MvcNewtonsoftJsonOptions>
{
   private readonly IHttpContextAccessor _provider;

   public MvcJsonOptionsSetup(IHttpContextAccessor provider)
   {
   }

   public MvcJsonOptionsSetup(IHttpContextAccessor provider)
   {
       _provider = provider;
   }

   public Func<IServiceProvider> GetProvider()
   {
       return () => _provider.HttpContext?.RequestServices;
   }

   public override void Configure(MvcNewtonsoftJsonOptions options)
   {
       options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
       options.SerializerSettings.ContractResolver = new DefaultContractResolver();
       options.SerializerSettings.ConfigureRepositoryForJson<EntidadeBase>(GetProvider());
       options.SerializerSettings.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Auto;
    }
}

The problem is with the arrays; I would like to never put the $values, but sometimes the return type is not the same.

Did anyone make some extension that would be TypeNAmeHandling.Auto except for arrays?

For example, I use the kendo grid, and the return is this:

{
  "Data": {
    "$type": "System.Collections.Generic.List`1[[Model.Entity, Model]], System.Private.CoreLib",
    "$values": [
        ...
    ]
  },
  "Groups": null,
  "Aggregates": null,
  "Total": 4,
  "Errors": null
}

And I would like it as this

{
  "Data":  [
        ...
    ]
  ,
  "Groups": null,
  "Aggregates": null,
  "Total": 4,
  "Errors": null
} 

I'm not using the

TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects 

to make the result even simpler, I would like to return only if the type is a subclass.

This is the result class

public class DataSourceResult
{
    public IEnumerable Data { get; set; }
    public IEnumerable Groups { get; set; }
    public object Aggregates { get; set; }
    public int Total { get; set; }
    public object Errors { get; set; }
}

Finally, note that I can't put attributes (such as [JsonProperty(TypeNameHandling=TypeNameHandling.None)]) in all classes, I mean, there are libraries that I use and can't change, I was looking for something that I could override the newtonsoft serializer and check if was array and ignore the typeNamehandling property.

1 Answers

With Json.NET there is no way to enable or disable emission of type information based purely on the concrete type of the item being serialized. Instead, emission of type information is controlled by the member or collection that refers to the item being serialized.*

As long as those members (or collection items) are declared to be something assignable from IEnumerable, you can suppress type information using a custom contract resolver such as the following:

public class SuppressCollectionTypeNamesContractResolver : DefaultContractResolver
{
    internal static TypeNameHandling? RemoveCollectionTypenameHandling(TypeNameHandling? typeNameHandling, Type valueType)
        => typeNameHandling ?? (typeof(IEnumerable).IsAssignableFrom(valueType) && valueType != typeof(string) ? TypeNameHandling.None : typeNameHandling);

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        property.TypeNameHandling = RemoveCollectionTypenameHandling(property.TypeNameHandling, property.PropertyType);
        return property;
    }

    protected override JsonArrayContract CreateArrayContract(Type objectType)
    {
        var contract = base.CreateArrayContract(objectType);
        contract.ItemTypeNameHandling = RemoveCollectionTypenameHandling(contract.ItemTypeNameHandling, contract.CollectionItemType);
        return contract;
    }
}

And use it in place of DefaultContractResolver like so:

options.SerializerSettings.ContractResolver = new SuppressCollectionTypeNamesContractResolver();

Note that Newtonsoft recommends caching custom resolvers statically for best performance.

Demo fiddle #1 here.

If you might have members declared as object or dynamic and still need to suppress type information in cases where their values are collections, you will have to use a custom converter such as the following:

public class SuppressCollectionTypeNamesConverter : JsonConverter
{
    static readonly IContractResolver globalDefaultResolver = new JsonSerializer().ContractResolver;
    IContractResolver resolver;
    
    public SuppressCollectionTypeNamesConverter() : this(globalDefaultResolver) { }
    public SuppressCollectionTypeNamesConverter(IContractResolver resolver) => this.resolver = resolver ?? globalDefaultResolver;
    
    public override bool CanConvert(Type objectType) => typeof(IEnumerable).IsAssignableFrom(objectType) && objectType != typeof(string) && resolver.ResolveContract(objectType) is JsonArrayContract;

    public override bool CanRead => false;      
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => throw new NotImplementedException();        

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var contract = (JsonArrayContract)serializer.ContractResolver.ResolveContract(value.GetType());
        var itemType = contract.CollectionItemType;
        writer.WriteStartArray();
        foreach (object item in ((IEnumerable)value))
        {
            if (contract.ItemTypeNameHandling == TypeNameHandling.None)
                serializer.Serialize(writer, item);
            else
                serializer.Serialize(writer, item, itemType);
        }
        
        writer.WriteEndArray();
    }
}

And add it to options as follows:

options.SerializerSettings.Converters.Add(new SuppressCollectionTypeNamesConverter());

Demo fiddle #2 here.


* This can be confirmed from the reference source for JsonSerializerInternalWriter.SerializeList() which calls JsonSerializerInternalWriter.ShouldWriteType()

Related