I find myself doing this a lot. I have a class that looks something like this:
public class Foo
{
public SomeEnum SomeValue { get; set; }
public SomeAbst
According to the JSON specification, a JSON object is "an unordered set of name/value pairs", so trying to access the parent's SomeValue
enum while reading an instance of SomeAbstractBaseClass
isn't guaranteed to work -- as it might not have been read yet.
So, I'd first like to suggest a couple of alternative designs. Since Json.NET is basically a contract serializer, it will be easier to use if the polymorphic object itself conveys its type information, rather than parent container objects. Thus you could either:
Move the polymorphic type enum into SomeAbstractBaseClass
along the lines of Json.Net Serialization of Type with Polymorphic Child Object.
Use Json.NET's built-in support for polymorphic types by setting JsonSerializerSettings.TypeNameHandling to TypeNameHandling.Auto.
That being said, you can reduce your pain somewhat by, inside a JsonConverter, reading the JSON for your container class Foo
into a JObject
, splitting out the polymorphic properties for custom handling, and using JsonSerializer.Populate to fill in the remaining properties. You can even standardize this pattern by creating an abstract converter that does this for you, using a custom attribute to determine which properties to split out:
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field, AllowMultiple = false)]
public sealed class JsonCustomReadAttribute : Attribute
{
}
public abstract class JsonCustomReadConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var contract = serializer.ContractResolver.ResolveContract(objectType) as JsonObjectContract;
if (contract == null)
throw new JsonSerializationException("invalid type " + objectType.FullName);
var value = existingValue ?? contract.DefaultCreator();
var jObj = JObject.Load(reader);
// Split out the properties requiring custom handling
var extracted = contract.Properties
.Where(p => p.AttributeProvider.GetAttributes(typeof(JsonCustomReadAttribute), true).Count > 0)
.Select(p => jObj.ExtractProperty(p.PropertyName))
.Where(t => t != null)
.ToList();
// Populare the properties not requiring custom handling.
using (var subReader = jObj.CreateReader())
serializer.Populate(subReader, value);
ReadCustom(value, new JObject(extracted), serializer);
return value;
}
protected abstract void ReadCustom(object value, JObject jObject, JsonSerializer serializer);
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
public static class JsonExtensions
{
public static JProperty ExtractProperty(this JObject obj, string name)
{
if (obj == null)
throw new ArgumentNullException();
var property = obj.Property(name);
if (property == null)
return null;
property.Remove();
return property;
}
}
And then use it like:
public abstract class SomeAbstractBaseClass
{
}
public class Class1 : SomeAbstractBaseClass
{
public string Value1 { get; set; }
}
public class Class2 : SomeAbstractBaseClass
{
public string Value2 { get; set; }
}
public static class SomeAbstractBaseClassSerializationHelper
{
public static SomeEnum SerializedType(this SomeAbstractBaseClass baseObject)
{
if (baseObject == null)
return SomeEnum.None;
if (baseObject.GetType() == typeof(Class1))
return SomeEnum.Class1;
if (baseObject.GetType() == typeof(Class2))
return SomeEnum.Class2;
throw new InvalidDataException();
}
public static SomeAbstractBaseClass DeserializeMember(JObject jObject, string objectName, string enumName, JsonSerializer serializer)
{
var someObject = jObject[objectName];
if (someObject == null || someObject.Type == JTokenType.Null)
return null;
var someValue = jObject[enumName];
if (someValue == null || someValue.Type == JTokenType.Null)
throw new JsonSerializationException("no type information");
switch (someValue.ToObject<SomeEnum>(serializer))
{
case SomeEnum.Class1:
return someObject.ToObject<Class1>(serializer);
case SomeEnum.Class2:
return someObject.ToObject<Class2>(serializer);
default:
throw new JsonSerializationException("unexpected type information");
}
}
}
public enum SomeEnum
{
None,
Class1,
Class2,
}
[JsonConverter(typeof(FooConverter))]
public class Foo
{
[JsonCustomRead]
public SomeEnum SomeValue { get { return SomeObject.SerializedType(); } }
[JsonCustomRead]
public SomeAbstractBaseClass SomeObject { get; set; }
public string SomethingElse { get; set; }
}
public class FooConverter : JsonCustomReadConverter
{
protected override void ReadCustom(object value, JObject jObject, JsonSerializer serializer)
{
var foo = (Foo)value;
foo.SomeObject = SomeAbstractBaseClassSerializationHelper.DeserializeMember(jObject, "SomeObject", "SomeValue", serializer);
}
public override bool CanConvert(Type objectType)
{
return typeof(Foo).IsAssignableFrom(objectType);
}
}