I can't seem to find a good answer so far, but I acknowledge maybe I'm just not smart enough to know the right keywords to search. So here goes.
Suppose I have a collection containing mixed object types:
var wishList = new List<WishListItem>
{
new Car { Price = 78000, Make = "Tesla", Model = "S", Name = "Tesla Model S" },
new Computer { Manufacturer = "Microsoft", Name = "Surface Pro 6", Price = 2000 },
new PlatitudeIdea { Name = "World peace" }
};
As a collection built in memory, I can use casting to handle these objects according to their underlying types:
foreach (var wishListItem in wishList)
{
if (wishListItem is PlatitudeIdea platitude)
{
Console.WriteLine($"{platitude.Name} is a hopeless dream");
}
else if (wishListItem is IPriceable priceThing)
{
Console.WriteLine($"At {priceThing.Price}, {priceThing.Name} is way out of my budget");
}
else
{
Console.WriteLine($"I want a {wishListItem.Name}");
}
}
If I serialize it as a JSON array, everything looks fine...
[
{ "Price": 78000, "Make": "Tesla", "Model": "S", "Name": "Tesla Model S" },
{ "Manufacturer ": "Microsoft", "Name": "Surface Pro 6", "Price": 2000 },
{ "Name": "World peace" }
]
... but when I parse the JSON, the parser obviously can't tell exactly which type each element was originally, so it just tries to parse them as the lowest level type declared in the List's generic parameter (WishListItem) as I would expect:
parsedWishList[0] is WishListitem // returns true :)
parsedWishList[0] is Car // returns false :(
This makes sense, and you could get this behavior any time the member being serialized is declared as a super type or interface. What I would love to be able to do is add a special property to my specific classes, indicating the type of the object being serialized:
public class Car : WishListItem, IPriceable
{
public override string @type => "Car";
}
Or better yet, as a type attribute:
[JsonSerializedType("Car")]
public class Car : WishListItem, IPriceable
{
// ...
}
This would then be output to the JSON whenever the type declaration is ambiguous...
[
{ "type": "Car", "Price": 78000, "Make": "Tesla", "Model": "S" },
{ "type": "Computer", "Manufacturer ": "Microsoft", "Name": "Surface Pro 6", "Price": 2000 },
{ "type": "Platitude", "Value": "World peace" }
]
... And the parser would deserialize that object as that type:
parsedWishList[0] is Car // returns true :)
The closest thing to an answer i was able to glean from Google was maybe to try playing with the CustomCreationConverter and see if that might help somehow. But I need a very generic answer I can write once and let it handle arbitrary types.
Any pointers?
It sounds like you are looking for the TypeNameHandling setting. This setting will cause Json.Net to write type information into the JSON so that it is deserialized back to its original type.
If you need to customize the type names, you can use a custom SerializationBinder class.
Here is a round-trip demo based on the KnownTypesBinder sample shown in the documentation:
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace SO54465235
{
public class Program
{
public static void Main(string[] args)
{
var wishList = new List<WishListItem>
{
new Car { Price = 78000, Make = "Tesla", Model = "S", Name = "Tesla Model S" },
new Computer { Manufacturer = "Microsoft", Name = "Surface Pro 6", Price = 2000 },
new Platitude { Name = "World peace" }
};
KnownTypesBinder knownTypesBinder = new KnownTypesBinder
{
KnownTypes = new List<Type> { typeof(Car), typeof(Computer), typeof(Platitude) }
};
string json = JsonConvert.SerializeObject(wishList, Formatting.Indented, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Objects,
SerializationBinder = knownTypesBinder
});
Console.WriteLine(json);
Console.WriteLine();
List<WishListItem> items = JsonConvert.DeserializeObject<List<WishListItem>>(json, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Objects,
SerializationBinder = knownTypesBinder
});
foreach (var wishListItem in wishList)
{
if (wishListItem is Platitude platitude)
{
Console.WriteLine($"{platitude.Name} is a hopeless dream");
}
else if (wishListItem is IPriceable priceThing)
{
Console.WriteLine($"At {priceThing.Price}, {priceThing.Name} is way out of my budget");
}
else
{
Console.WriteLine($"I want a {wishListItem.Name}");
}
}
}
}
public class KnownTypesBinder : ISerializationBinder
{
public IList<Type> KnownTypes { get; set; }
public Type BindToType(string assemblyName, string typeName)
{
return KnownTypes.SingleOrDefault(t => t.Name == typeName);
}
public void BindToName(Type serializedType, out string assemblyName, out string typeName)
{
assemblyName = null;
typeName = serializedType.Name;
}
}
class WishListItem
{
public string Name { get; set; }
}
interface IPriceable
{
int Price { get; set; }
string Name { get; set; }
}
class Car : WishListItem, IPriceable
{
public string Make { get; set; }
public string Model { get; set; }
public int Price { get; set; }
}
class Computer : WishListItem, IPriceable
{
public string Manufacturer { get; set; }
public int Price { get; set; }
}
class Platitude : WishListItem
{
}
}
Output:
[
{
"$type": "Car",
"Make": "Tesla",
"Model": "S",
"Price": 78000,
"Name": "Tesla Model S"
},
{
"$type": "Computer",
"Manufacturer": "Microsoft",
"Price": 2000,
"Name": "Surface Pro 6"
},
{
"$type": "Platitude",
"Name": "World peace"
}
]
At 78000, Tesla Model S is way out of my budget
At 2000, Surface Pro 6 is way out of my budget
World peace is a hopeless dream
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With