14

I want to modify my json.NET serializer to add the $type property only to the objects which implements a given interface but not to any property or nested objects.

With TypeNameHandling.Auto (default)

{
  "PropertyA": 123,
  "PropertyB": "foo",
  "PropertyC": [1, 2, 3, 4]
}

With TypeNameHandling.All

{
  "$type": "JsonNetTypeNameHandling.TestEvent, jsonNetTypeNameHandling",
  "PropertyA": 123,
  "PropertyB": "foo",
  "PropertyC": {
    "$type": "System.Collections.Generic.List`1[[System.Int32, mscorlib]], mscorlib",
    "$values": [1, 2, 3, 4 ]
  }
}

What I want

{
  "$type": "JsonNetTypeNameHandling.TestEvent, jsonNetTypeNameHandling",
  "PropertyA": 123,
  "PropertyB": "foo",
  "PropertyC": [1, 2, 3, 4]
}

I am experimenting with a custom ContractResolver but I don't get it to work:

class Program
{
    static void Main(string[] args)
    {
        var serializerSettings = new JsonSerializerSettings()
        {
            TypeNameHandling = TypeNameHandling.Auto,
            TypeNameAssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple,
            NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore,
            ContractResolver = new EnableTypeNameHandlingAllOnlyForEvents(),
            Formatting = Formatting.Indented
        };

        var event1 = new TestEvent() { PropertyA = 123, PropertyB = "foo", PropertyC = new List<int> { 1, 2, 3, 4 } };

        string event1Serialized = JsonConvert.SerializeObject(event1, serializerSettings);

        Console.WriteLine(event1Serialized);
        Console.ReadLine();
    }
}

public interface IEvent
{
}

public class TestEvent : IEvent
{
    public int PropertyA { get; set; }
    public string PropertyB { get; set; }
    public List<int> PropertyC { get; set; }
}

public class EnableTypeNameHandlingAllOnlyForEvents : DefaultContractResolver
{
    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        var x = base.CreateObjectContract(objectType);

        if (typeof(IEvent).IsAssignableFrom(x.UnderlyingType))
        {
            // What to do to tell json.NET to add $type to instances of this (IEvent) type???
        }

        return x;
    }
}
Tobias J.
  • 1,043
  • 12
  • 31
  • To force a `"$type"` on the root object with `TypeNameHandling.Auto`, see [Serializing an interface/abstract object using NewtonSoft.JSON](https://stackoverflow.com/questions/28128923/serializing-an-interface-abstract-object-using-newtonsoft-json). However, this leaves open the possibility of polymorphic properties and array entries getting a `"$type"` also. Is that OK? – dbc Apr 01 '16 at 16:05

2 Answers2

14

If you require the "$type" property on your root object and are OK with it appearing on nested polymorphic objects and arrays if required, use the following overload along with TypeNameHandling.Auto: JsonConvert.SerializeObject(Object, Type, JsonSerializerSettings).

From the docs:

public static string SerializeObject(
    Object value,
    Type type,
    JsonSerializerSettings settings
)

type Type: System.Type The type of the value being serialized. This parameter is used when TypeNameHandling is Auto to write out the type name if the type of the value does not match. Specifing the type is optional.

I.e., do:

var serializerSettings = new JsonSerializerSettings()
{
    TypeNameHandling = TypeNameHandling.Auto,
    TypeNameAssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple,
    NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore,
    Formatting = Formatting.Indented
};

var event1Serialized = JsonConvert.SerializeObject(event1, typeof(IEvent), serializerSettings);

If you require "$type" on the root object and will not accept it on nested polymorphic objects and arrays even if otherwise required, you will need to use TypeNameHandling.All along with a custom contract resolver that sets JsonContainerContract.ItemTypeNameHandling = TypeNameHandling.None:

public class SuppressItemTypeNameContractResolver : DefaultContractResolver
{
    protected override JsonContract CreateContract(Type objectType)
    {
        var contract = base.CreateContract(objectType);
        var containerContract = contract as JsonContainerContract;
        if (containerContract != null)
        {
            if (containerContract.ItemTypeNameHandling == null)
                containerContract.ItemTypeNameHandling = TypeNameHandling.None; 
        }
        return contract;
    }
}

Then use it like:

static IContractResolver suppressItemTypeNameContractResolver = new SuppressItemTypeNameContractResolver();

var serializerSettings = new JsonSerializerSettings()
{
    TypeNameHandling = TypeNameHandling.All,
    ContractResolver = suppressItemTypeNameContractResolver,
    // Other settings as required.
    TypeNameAssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple,
    NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore,
    Formatting = Formatting.Indented
};
var event1Serialized = JsonConvert.SerializeObject(event1, serializerSettings);

Notes:

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Great. Your second suggestion (SuppressItemTypeNameContractResolver) is exactly what I need. Thank you! – Tobias J. Apr 03 '16 at 06:59
  • +1. This still feels hacky, but I haven't found a better way to make it work when using ObjectContent in AspNetCore. The answer here, [link](http://stackoverflow.com/a/28133806/967448), where you explicitly call the Serialize method, but pass a base type, works, but it won't work when the framework is using the configured serializer. – rrreee Aug 24 '16 at 16:03
  • @rrreee - yes, lot of troubles just for adding the type info for the root. A more simple hack: just wrap the root object in a typed list ( new List(){rootObject} ) for serialization and on deserialization get the first element of the list that will have the correct type. – Fabianus Nov 14 '20 at 21:16
0

Simpler solution is to override the TypeHandling on property level

public class TestEvent : IEvent
{
    public int PropertyA { get; set; }
    public string PropertyB { get; set; }

    [JsonProperty(TypeNameHandling = TypeNameHandling.None)]
    public List<int> PropertyC { get; set; }
}
Manuel Amstutz
  • 1,311
  • 13
  • 33