ARTICLE AD BOX
You would like a way to serialize a model using Json.NET that handles polymorphic properties in the same way that the XmlSerializer can handle polymorphic properties:
For each property, the set of possible polymorphic subtypes is known at compile time and can be specified via a list of attributes, similarly to how XmlElementAttribute.Type works:
public partial class Model { [XmlElement("A", typeof(A)), XmlElement("B", typeof(B))] public object Item { get; set; } }When serializing a polymorphic property to JSON, the name used should be the name specified in the attribute list corresponding to the actual, concrete type of the property value currently being serialized.
When deserializing, when a polymorphic property name is encountered, the value in the JSON should be deserialized to the type specified via attributes.
Given those requirements, a solution using a JsonConverter such as this one by Brian Rogers to How to change property names depending on the type when serializing with Json.net? would not be convenient. Instead, I would suggest creating a custom contract resolver that inherits from DefaultContractResolver that adds in appropriately renamed synthetic properties corresponding to the possible types of values for each polymorphic property, then uses conditional serialization to only serialize the single property corresponding to the current value's type. Then during deserialization the serializer will be able to infer the type automatically from the synthetic property name.
First introduce the following attribute:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] public sealed class JsonPolymorphicNameAttribute : System.Attribute { public JsonPolymorphicNameAttribute(string polymorphicName, Type polymorphicType) { this.PolymorphicName = polymorphicName; this.PolymorphicType = polymorphicType; } public string PolymorphicName { get; private set; } public Type PolymorphicType { get; private set; } }Next create the following contract resolver:
public class JsonPolymorphicNameAttributeResolver : DefaultContractResolver { protected override JsonObjectContract CreateObjectContract(Type objectType) { var contract = base.CreateObjectContract(objectType); for (int i = 0; i < contract.Properties.Count; i++) { var property = contract.Properties[i]; var attributeProvider = property.AttributeProvider; var valueProvider = property.ValueProvider; if (attributeProvider == null || valueProvider == null || property.Ignored) continue; var attrs = attributeProvider.GetAttributes(typeof(JsonPolymorphicNameAttribute), true); if (attrs.Count == 0) continue; if (property.PropertyName != null && contract.CreatorParameters.GetClosestMatchProperty(property.PropertyName) != null) { // TODO: decide how to handle polymorphic property names in parameterized objects where the polymorphically named property needs to be passed to the constructor. // Unfortunately Json.NET matches constructor parameters to JSON properties by name which is inconsistent with polymorphic property naming. throw new ArgumentException(string.Format("Polymorphically named properties are not supported for parameterized constructors: property \"{0}\", type \"{1}\"", property.PropertyName, contract.UnderlyingType)); } var polymorphicTypes = new HashSet<Type>(attrs.Count); foreach (var attr in attrs.Cast<JsonPolymorphicNameAttribute>()) { if (property.PropertyType != null && !property.PropertyType.IsAssignableFrom(attr.PolymorphicType)) { throw new ArgumentException(string.Format("JsonProperty.PropertyType {0} is not assignable from JsonPolymorphicNameAttribute.PolymorphicType {1}", property.PropertyType, attr.PolymorphicType)); } var newProperty = property.ShallowClone(); newProperty.PropertyName = attr.PolymorphicName; newProperty.PropertyType = attr.PolymorphicType; if (property.Readable) { newProperty.ShouldSerialize = newProperty.ShouldSerialize.And( o => { var value = valueProvider.GetValue(o); return value != null && value.GetType() == attr.PolymorphicType; }); } else { newProperty.ShouldSerialize = o => false; } contract.Properties.Insert(++i, newProperty); polymorphicTypes.Add(attr.PolymorphicType); } property.ShouldSerialize = property.ShouldSerialize.And( o => { var value = valueProvider.GetValue(o); // TODO: decide what to do if the value is null, since we can't get the concretetype. return value != null && !polymorphicTypes.Contains(value.GetType()); }); } return contract; } } public static partial class JsonExtensions { static readonly Func<JsonProperty, JsonProperty> ShallowClonePropertyFunc = CreateShallowCloneMethod<JsonProperty>(); public static JsonProperty ShallowClone(this JsonProperty property) { if (property == null) throw new ArgumentNullException("property"); return ShallowClonePropertyFunc(property); } internal static Predicate<T> And<T>(this Predicate<T> first, Predicate<T> second) { if (second == null) return first; else if (first == null) return second; else return v => first(v) && second(v); } internal static Func<T, T> CreateShallowCloneMethod<T>() { var method = typeof(T).GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); if (method == null) throw new ArgumentException(string.Format("No MemberwiseClone() method was found for type {0}", typeof(T))); var cloneUntyped = (Func<T, object>)Delegate.CreateDelegate(typeof(Func<T, object>), method); return delegate(T obj) { return (T)cloneUntyped(obj); }; } }Then, to use it, modify your model and apply [JsonPolymorphicName] as required:
public class Model { // Other properties not shown in the question [JsonPolymorphicName("A", typeof(A))] [JsonPolymorphicName("B", typeof(B))] [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] // Other JsonPropertyAttribute settings which must needs be retained. public object Item { get; set; } }Then construct an instance of JsonPolymorphicNameAttributeResolver with your required naming strategy:
DefaultContractResolver resolver = new JsonPolymorphicNameAttributeResolver() { NamingStrategy = new CamelCaseNamingStrategy(), };Then use it in JsonSerializerSettings.ContractResolver when serializing and deserializing an instance of Model like so:
var model = new Model { Item = new B { B1 = 13, B2 = "value of B2" }, }; var settings = new JsonSerializerSettings { ContractResolver = resolver, // Add any other settings you require }; var json = JsonConvert.SerializeObject(model, settings); var model2 = JsonConvert.DeserializeObject<Model>(json, settings);Now the model will be round-tripped as {"B":{"b1":13,"b2":"value of B2"}} and the type will be inferred correctly during deserialization.
Notes:
JsonProperty.ValueProvider has a method IValueProvider.GetValue(object target) that returns the value of the property for a specific instance.
Thus, inside the Predicate<object> specified by JsonProperty.ShouldSerialize, we can use the value provider to get the current property value to determine its type and thus whether it should be serialized by the current polymorphic property.
Polymorphic constructor parameters are not supported. If one of your polymorphic properties corresponds to a constructor parameter then an ArgumentException will be thrown.
I'm not sure what you would want to do when a polymorphic property has a null value. In the code above I disable serialization.
For best performance, Newtonsoft recommends to cache and reuse your contract resolver instances.
I wrote the code above using older C# syntax in case you are coding against .NET Framework.
I used a custom attribute to specify polymorphic name, but you could try using XmlElementAttribute instead if you prefer. To get the set of attributes of a specific type for a given property, you can use JsonProperty.AttributeProvider, i.e.:
var attrs = property.AttributeProvider?.GetAttributes(typeof(System.Xml.Serialization.XmlElementAttribute), true);Demo .NET Framework fiddle here.
