Deserializing Polymorphic Json Classes Without Type Information Using Json.Net

Deserializing polymorphic json classes without type information using json.net

You can do this fairly easily by creating a custom JsonConverter to handle the object instantiation. Assuming you have your classes defined something like this:

public abstract class GalleryItem
{
public string id { get; set; }
public string title { get; set; }
public string link { get; set; }
public bool is_album { get; set; }
}

public class GalleryImage : GalleryItem
{
// ...
}

public class GalleryAlbum : GalleryItem
{
public int images_count { get; set; }
public List<GalleryImage> images { get; set; }
}

You would create the converter like this:

public class GalleryItemConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(GalleryItem).IsAssignableFrom(objectType);
}

public override object ReadJson(JsonReader reader,
Type objectType, object existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);

// Using a nullable bool here in case "is_album" is not present on an item
bool? isAlbum = (bool?)jo["is_album"];

GalleryItem item;
if (isAlbum.GetValueOrDefault())
{
item = new GalleryAlbum();
}
else
{
item = new GalleryImage();
}

serializer.Populate(jo.CreateReader(), item);

return item;
}

public override bool CanWrite
{
get { return false; }
}

public override void WriteJson(JsonWriter writer,
object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}

Here's an example program showing the converter in action:

class Program
{
static void Main(string[] args)
{
string json = @"
[
{
""id"": ""OUHDm"",
""title"": ""My most recent drawing. Spent over 100 hours."",
""link"": ""http://i.imgur.com/OUHDm.jpg"",
""is_album"": false
},
{
""id"": ""lDRB2"",
""title"": ""Imgur Office"",
""link"": ""http://alanbox.imgur.com/a/lDRB2"",
""is_album"": true,
""images_count"": 3,
""images"": [
{
""id"": ""24nLu"",
""link"": ""http://i.imgur.com/24nLu.jpg""
},
{
""id"": ""Ziz25"",
""link"": ""http://i.imgur.com/Ziz25.jpg""
},
{
""id"": ""9tzW6"",
""link"": ""http://i.imgur.com/9tzW6.jpg""
}
]
}
]";

List<GalleryItem> items =
JsonConvert.DeserializeObject<List<GalleryItem>>(json,
new GalleryItemConverter());

foreach (GalleryItem item in items)
{
Console.WriteLine("id: " + item.id);
Console.WriteLine("title: " + item.title);
Console.WriteLine("link: " + item.link);
if (item.is_album)
{
GalleryAlbum album = (GalleryAlbum)item;
Console.WriteLine("album images (" + album.images_count + "):");
foreach (GalleryImage image in album.images)
{
Console.WriteLine(" id: " + image.id);
Console.WriteLine(" link: " + image.link);
}
}
Console.WriteLine();
}
}
}

And here is the output of the above program:

id: OUHDm
title: My most recent drawing. Spent over 100 hours.
link: http://i.imgur.com/OUHDm.jpg

id: lDRB2
title: Imgur Office
link: http://alanbox.imgur.com/a/lDRB2
album images (3):
id: 24nLu
link: http://i.imgur.com/24nLu.jpg
id: Ziz25
link: http://i.imgur.com/Ziz25.jpg
id: 9tzW6
link: http://i.imgur.com/9tzW6.jpg

Fiddle: https://dotnetfiddle.net/1kplME

Polymorphic JSON Deserialization failing using Json.Net

Firstly, SourceType is missed for menu item "Submenus Test" in your json.

Secondly, you shouldn't simply use ToObject because of the Submenus property, which should be handled recursively.

The following ReadJson will work:

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jObject = JObject.Load(reader);
var sourceType = jObject["SourceType"].Value<string>();

object target = null;

switch (sourceType)
{
case SourceType.File:
target = new FileMenu(); break;
case SourceType.Folder:
target = new FolderMenu(); break;
case SourceType.Json:
target = new JsonMenu(); break;
case SourceType.RestGet:
target = new RestMenu(); break;
case SourceType.Rss:
target = new RssMenu(); break;
case SourceType.Text:
target = new TextMenu(); break;
case SourceType.Url:
target = new UrlMenu(); break;
default:
throw new ArgumentException("Invalid source type");
}

serializer.Populate(jObject.CreateReader(), target);

return target;
}

Is polymorphic deserialization possible in System.Text.Json?

Is polymorphic deserialization possible in System.Text.Json?

The answer is yes and no, depending on what you mean by "possible".

There is no polymorphic deserialization (equivalent to Newtonsoft.Json's TypeNameHandling) support built-in to System.Text.Json. This is because reading the .NET type name specified as a string within the JSON payload (such as $type metadata property) to create your objects is not recommended since it introduces potential security concerns (see https://github.com/dotnet/corefx/issues/41347#issuecomment-535779492 for more info).

Allowing the payload to specify its own type information is a common source of vulnerabilities in web applications.

However, there is a way to add your own support for polymorphic deserialization by creating a JsonConverter<T>, so in that sense, it is possible.

The docs show an example of how to do that using a type discriminator property:
https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to#support-polymorphic-deserialization

Let's look at an example.

Say you have a base class and a couple of derived classes:

public class BaseClass
{
public int Int { get; set; }
}
public class DerivedA : BaseClass
{
public string Str { get; set; }
}
public class DerivedB : BaseClass
{
public bool Bool { get; set; }
}

You can create the following JsonConverter<BaseClass> that writes the type discriminator while serializing and reads it to figure out which type to deserialize. You can register that converter on the JsonSerializerOptions.

public class BaseClassConverter : JsonConverter<BaseClass>
{
private enum TypeDiscriminator
{
BaseClass = 0,
DerivedA = 1,
DerivedB = 2
}

public override bool CanConvert(Type type)
{
return typeof(BaseClass).IsAssignableFrom(type);
}

public override BaseClass Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}

if (!reader.Read()
|| reader.TokenType != JsonTokenType.PropertyName
|| reader.GetString() != "TypeDiscriminator")
{
throw new JsonException();
}

if (!reader.Read() || reader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}

BaseClass baseClass;
TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
switch (typeDiscriminator)
{
case TypeDiscriminator.DerivedA:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA));
break;
case TypeDiscriminator.DerivedB:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB));
break;
default:
throw new NotSupportedException();
}

if (!reader.Read() || reader.TokenType != JsonTokenType.EndObject)
{
throw new JsonException();
}

return baseClass;
}

public override void Write(
Utf8JsonWriter writer,
BaseClass value,
JsonSerializerOptions options)
{
writer.WriteStartObject();

if (value is DerivedA derivedA)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedA);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedA);
}
else if (value is DerivedB derivedB)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedB);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedB);
}
else
{
throw new NotSupportedException();
}

writer.WriteEndObject();
}
}

This is what serialization and deserialization would look like (including comparison with Newtonsoft.Json):

private static void PolymorphicSupportComparison()
{
var objects = new List<BaseClass> { new DerivedA(), new DerivedB() };

// Using: System.Text.Json
var options = new JsonSerializerOptions
{
Converters = { new BaseClassConverter() },
WriteIndented = true
};

string jsonString = JsonSerializer.Serialize(objects, options);
Console.WriteLine(jsonString);
/*
[
{
"TypeDiscriminator": 1,
"TypeValue": {
"Str": null,
"Int": 0
}
},
{
"TypeDiscriminator": 2,
"TypeValue": {
"Bool": false,
"Int": 0
}
}
]
*/

var roundTrip = JsonSerializer.Deserialize<List<BaseClass>>(jsonString, options);


// Using: Newtonsoft.Json
var settings = new Newtonsoft.Json.JsonSerializerSettings
{
TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects,
Formatting = Newtonsoft.Json.Formatting.Indented
};

jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(objects, settings);
Console.WriteLine(jsonString);
/*
[
{
"$type": "PolymorphicSerialization.DerivedA, PolymorphicSerialization",
"Str": null,
"Int": 0
},
{
"$type": "PolymorphicSerialization.DerivedB, PolymorphicSerialization",
"Bool": false,
"Int": 0
}
]
*/

var originalList = JsonConvert.DeserializeObject<List<BaseClass>>(jsonString, settings);

Debug.Assert(originalList[0].GetType() == roundTrip[0].GetType());
}

Here's another StackOverflow question that shows how to support polymorphic deserialization with interfaces (rather than abstract classes), but a similar solution would apply for any polymorphism:
Is there a simple way to manually serialize/deserialize child objects in a custom converter in System.Text.Json?

Deserialize JSON to .NET collection of polymorphic objects that contain Point values formatted as strings

Your remaining problem is that you are trying to deserialize a string containing a pair of numbers to a System.Windows.Point when the numbers have been formatted in a locale using a comma for a decimal separator. You will need to create a custom JsonConverter for this situation:

public class PointConverter : JsonConverter
{
readonly NumberFormatInfo numberFormatInfo = new NumberFormatInfo
{
NumberDecimalSeparator = ",",
NumberGroupSeparator = ".",
};

public override bool CanConvert(Type objectType)
{
return objectType == typeof(Point) || objectType == typeof(Point?);
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
switch (reader.MoveToContentAndAssert().TokenType)
{
case JsonToken.Null:
return null;

case JsonToken.String:
{
var s = (string)reader.Value;

var values = s.Split(';');
if (values.Length != 2)
throw new JsonSerializationException(string.Format("Invalid Point format {0}", s));
try
{
return new Point(double.Parse(values[0], numberFormatInfo), double.Parse(values[1], numberFormatInfo));
}
catch (Exception ex)
{
throw new JsonSerializationException(string.Format("Invalid Point format {0}", s), ex);
}
}

default:
throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
}
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(((Point)value).ToString(numberFormatInfo));
}
}

public static partial class JsonExtensions
{
public static JsonReader MoveToContentAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (reader.TokenType == JsonToken.None) // Skip past beginning of stream.
reader.ReadAndAssert();
while (reader.TokenType == JsonToken.Comment) // Skip past comments.
reader.ReadAndAssert();
return reader;
}

public static JsonReader ReadAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (!reader.Read())
throw new JsonReaderException("Unexpected end of JSON stream.");
return reader;
}
}

Then deserialize as follows:

var settings = new JsonSerializerSettings
{
Converters = { new PointConverter() },
};
var root = JsonConvert.DeserializeObject<TRootObject>(jsonString, settings);

You need to use a JsonConverter because, while Json.NET does support serializing objects as strings using their built-in TypeConverter, there seems to be a bug with the PointConverter used to convert Point to and from a string representation. Specifically, this built-in converter is culture-dependent when converting to a string representation, but culture-ignorant when converting back to a Point.

This can be seen in the reference source for PointConverter.ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value):

public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value == null)
{
throw GetConvertFromException(value);
}

String source = value as string;

if (source != null)
{
return Point.Parse(source);
}

return base.ConvertFrom(context, culture, value);
}

Notice that culture is not passed into Point.Parse()? Conversely, PointConverter.ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) does make make use of the incoming culture when formatting. That inconsistency is a bug.

And to confirm the bug, if you try to round-trip a Point in, say, the German culture, an exception will be thrown. The following code will throw, demonstrating the problem:

TypeDescriptor.GetConverter(typeof(Point)).ConvertFrom(null, CultureInfo.GetCultureInfo("de-DE"), TypeDescriptor.GetConverter(typeof(Point)).ConvertTo(null, CultureInfo.GetCultureInfo("de-DE"), new Point(-1.5, 3.4), typeof(string)))

Json.Net Serialization of Type with Polymorphic Child Object

Having the subtype information in the container class is problematic for two reasons:

  1. The container class instance is not accessible when Json.NET is reading the contained class.
  2. If you later need to convert the SubTypeClassBase property into, say, a list, there will be nowhere to put the subtype information.

Instead, I would recommend adding the subtype information as a property in the base class:

[JsonConverter(typeof(SubTypeClassConverter))]
public class SubTypeClassBase
{
[JsonConverter(typeof(StringEnumConverter))] // Serialize enums by name rather than numerical value
public SubType Type { get { return typeToSubType[GetType()]; } }
}

Now the custom subtype enum will be serialized whenever an object assignable to SubTypeClassBase is serialized. Having done that, for deserialization you can create a JsonConverter that loads the json for a given SubTypeClassBase into a temporary JObject, checks the value of the "Type" property, and deserializes the JSON object as the appropriate class.

Prototype implementation below:

public enum SubType
{
BaseType,
Type1,
Type2,
}

[JsonConverter(typeof(SubTypeClassConverter))]
public class SubTypeClassBase
{
static readonly Dictionary<Type, SubType> typeToSubType;
static readonly Dictionary<SubType, Type> subTypeToType;

static SubTypeClassBase()
{
typeToSubType = new Dictionary<Type,SubType>()
{
{ typeof(SubTypeClassBase), SubType.BaseType },
{ typeof(SubTypeClass1), SubType.Type1 },
{ typeof(SubTypeClass2), SubType.Type2 },
};
subTypeToType = typeToSubType.ToDictionary(pair => pair.Value, pair => pair.Key);
}

public static Type GetType(SubType subType)
{
return subTypeToType[subType];
}

[JsonConverter(typeof(StringEnumConverter))] // Serialize enums by name rather than numerical value
public SubType Type { get { return typeToSubType[GetType()]; } }
}

public class SubTypeClass1 : SubTypeClassBase
{
public string AaaField { get; set; }
}

public class SubTypeClass2 : SubTypeClassBase
{
public string ZzzField { get; set; }
}

public class SubTypeClassConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(SubTypeClassBase);
}

public override bool CanWrite { get { return false; } }

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var token = JToken.Load(reader);
var typeToken = token["Type"];
if (typeToken == null)
throw new InvalidOperationException("invalid object");
var actualType = SubTypeClassBase.GetType(typeToken.ToObject<SubType>(serializer));
if (existingValue == null || existingValue.GetType() != actualType)
{
var contract = serializer.ContractResolver.ResolveContract(actualType);
existingValue = contract.DefaultCreator();
}
using (var subReader = token.CreateReader())
{
// Using "populate" avoids infinite recursion.
serializer.Populate(subReader, existingValue);
}
return existingValue;
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}


Related Topics



Leave a reply



Submit