6

We're using Web API with Json.Net using TypeNameHandling = TypeNameHandling.Objects in our serializer settings. This works fine, but we use the type information only client-side, never for deserialization. Our serialized objects look like this:

{
    "$type": "PROJECTNAME.Api.Models.Directory.DtoName, PROJECTNAME.Api",
    "id": 67,
    "offices": [{
        "$type": "PROJECTNAME.Api.Models.Directory.AnotherDtoName, PROJECTNAME.Api",
        "officeName": "FOO"
    }]
},

I would like to customize the value in the $type property so it reads as:

{
    "$type": "Models.Directory.DtoName",
    "id": 67,
    "offices": [{
        "$type": "Models.Directory.AnotherDtoName",
        "officeName": "FOO"
    }]
},

I already have a contract resolver that inherits from CamelCasePropertyNamesContractResolver. I figure what I need to do is turn off TypeNameHandling and add a custom property myself. I'm 95% there:

protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
    var assemblyName = type.Assembly.GetName().Name;
    var typeName = type.FullName.Substring(assemblyName.Length + 1);

    var typeProperty = new JsonProperty()
    {
        PropertyName = "$type",
        PropertyType = typeof(string),
        Readable = true,
        Writable = true,
        ValueProvider = null // ????? typeName
    };

    var retval = base.CreateProperties(type, memberSerialization);
    retval.Add(typeProperty);
    return retval;
}

At this point I'm stuck with supplying the property's value.

I'm unsure that this is the correct approach because each of the ValueProvider types from Json.Net take a MemberInfo as a constructor parameter. I don't have a MemberInfo to supply as a parameter, so.... I'm stuck.

How do I add a custom $type value? Since I'm not doing deserialization in C# I will never need to convert the type information back into a type.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Don't add a `$type` property. Instead create a [custom `ISerializationBinder`](https://www.newtonsoft.com/json/help/html/SerializeSerializationBinder.htm) such as the ones in [JSON.Net: deserializing polymorphic types without specifying the assembly](https://stackoverflow.com/q/13598969/3744182), [Using a custom type discriminator to tell JSON.net which type of a class hierarchy to deserialize](https://stackoverflow.com/q/11099466/3744182) and [Newtonsoft.json serializing and deserializing base/inheirited where classes are from shared projects](https://stackoverflow.com/q/45923177/3744182). – dbc Mar 14 '18 at 19:49
  • I'm not doing deserialization in C#. –  Mar 14 '18 at 19:51
  • [`ISerializationBinder.BindToName`](https://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_Serialization_ISerializationBinder_BindToName.htm) is called during serialization of polymorphic types to specify the type information to emit for `$type`, and so should do what you want. – dbc Mar 14 '18 at 19:54
  • Ahhhhh. I was about to ask if I misunderstood the purpose of `ISerializationBinder`. I'll check that out. –  Mar 14 '18 at 19:54
  • Note that the docs are a little obsolete because recently Newtonsoft had to replace use of [`SerializationBinder`](https://msdn.microsoft.com/en-us/library/ffas09b2) which is apparently missing in some versions of .Net core with its own [`ISerializationBinder`](https://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_Serialization_ISerializationBinder.htm). If you are using Json.NET 10.0 or later, use ` ISerializationBinder`. – dbc Mar 14 '18 at 19:55

1 Answers1

6

Rather than adding a synthetic $type property, you should create a custom ISerializationBinder and override ISerializationBinder.BindToName. This method is called during serialization to specify the type information to emit when TypeNameHandling is enabled.

For instance, the following strips the assembly information as well as the PROJECTNAME.Api. portion of the namespace:

public class MySerializationBinder : ISerializationBinder
{
    const string namespaceToRemove = "PROJECTNAME.Api.";

    readonly ISerializationBinder binder;

    public MySerializationBinder() : this(new Newtonsoft.Json.Serialization.DefaultSerializationBinder()) { }

    public MySerializationBinder(ISerializationBinder binder)
    {
        if (binder == null)
            throw new ArgumentNullException();
        this.binder = binder;
    }

    #region ISerializationBinder Members

    public void BindToName(Type serializedType, out string assemblyName, out string typeName)
    {
        binder.BindToName(serializedType, out assemblyName, out typeName);
        if (typeName != null && typeName.StartsWith(namespaceToRemove))
            typeName = typeName.Substring(namespaceToRemove.Length);

        assemblyName = null;
    }

    public Type BindToType(string assemblyName, string typeName)
    {
        throw new NotImplementedException();
    }

    #endregion
}

Then you can serialize your DtoName object with it as follows:

var settings = new JsonSerializerSettings
{
    ContractResolver = new CamelCasePropertyNamesContractResolver(),
    SerializationBinder = new MySerializationBinder(),
    TypeNameHandling = TypeNameHandling.Objects,
};
var json = JsonConvert.SerializeObject(dto, Formatting.Indented, settings);

Notes:

dbc
  • 104,963
  • 20
  • 228
  • 340