In my project i receive a JSON which represents a Configuration for an device. Each device has one or more Interfaces that it can be connected with. In the property "SupportedInterfaces" i receive a List with all Interfaces that are supported for this device. The List expects the type BaseInterface but i'll receive the derived classes "RestInterface", "ModbusTcpInterface" or more to come.
I'm trying to deserialize List<BaseInterface> and directly convert it into the specific derived class type for further usage and to store in the DB, with the custom BaseInterfaceConverter. But that does not work like expected.
The following Code is my current state, i did not get it running - and for me it looks like it is because of the "nested" List inside the JSON that i'd like to deserialize. The Error in the Console is:
"Error reading JObject from JsonReader. Current JsonReader item is not an object: StartArray.
My code looks like this:
// The Parent Class
public class BaseInterface
{
public string Name { get; set; }
// This defines the specific interface-type
// TODO: change to an enumeration
public string InterfaceType { get; set; }
public string Option { get; set; }
}
// First Child
public class RestInterface : BaseInterface
{
public string DefaultBaseUri { get; set; }
}
// 2nd Child
public class ModbusTcpInterface: BaseInterface
{
public string IpAddress { get; set; }
public int Port { get; set; }
}
// The Configuration which holds a list of Interfaces that i would like to parse while deserializing.
public class DeviceConfiguration
{
public string DeviceName { get; set; }
public string DeviceManufacturer { get; set; }
[JsonConverter(typeof(BaseInterfaceConverter))]
public List<BaseInterface> SupportedInterfaces { get; set; }
//... more props here
}
public class BaseInterfaceConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(BaseInterface);
}
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
JObject jobject = JObject.Load(reader);
switch (jobject["InterfaceType"].Value<string>())
{
case "Rest":
return JsonConvert.DeserializeObject<RestInterface>(jobject.ToString());
case "ModbusTcp":
return JsonConvert.DeserializeObject<ModbusTcpInterface>(jobject.ToString());
default:
throw new ArgumentException(String.Format("The Interfacetype {0} is not supported!", jobject["InterfaceType"].Value<string>()));
}
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
}
What is that i'm missing? Can anyone please give me a hint?
Edit 1, added the JSON that I receive:
{
"_id": "1234",
"DeviceName": "First Configuration",
"DeviceManufacturer": "eeeitschi",
"SupportedInterfaces": [
{
"Name": "My first interface",
"InterfaceType": "Rest",
"Option": "option string here..",
"DefaultBaseUri": "mybaseurl.io/itsme",
},
{
"Name": "My second interface",
"InterfaceType": "ModbusTcp",
"Option": "option string here..",
"IpAddress": "127.0.0.1",
"Port": 502
},
{
"Name": "My third interface",
"InterfaceType": "Rest",
"Option": "option string here..",
"DefaultBaseUri": "base.url/api/devices",
},
]
}
Edit 2, rewrote the BaseInterfaceConverter after the answers from Max and Serge:
public class BaseInterfaceConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(BaseInterface);
}
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
JArray jarray = JArray.Load(reader);
return jarray.Select(entry => {
switch (entry["InterfaceType"].Value<string>())
{
case "Rest":
return JsonConvert.DeserializeObject<RestInterface>(entry.ToString());
case "ModbusTcp":
return JsonConvert.DeserializeObject<ModbusTcpInterface>(entry.ToString());
default:
throw new ArgumentException(String.Format("The Interfacetype {0} is not supported!", entry["InterfaceType"].Value<string>()));
}
}).ToList();
}
You can try this code
var jObject = JObject.Parse(json);
DeviceConfiguration deviceConfiguration = jObject.ToObject<DeviceConfiguration>();
deviceConfiguration.SupportedInterfaces = ((JArray)jObject["SupportedInterfaces"])
.Select(x => (string)x["InterfaceType"] == "Rest" ? (BaseInterface)x.ToObject<RestInterface>()
: (BaseInterface)x.ToObject<ModbusTcpInterface>())
.ToList();
or you can wrap it into converter
public class BaseClassConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(List<BaseInterface>);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JArray jArray = JArray.Load(reader);
return jArray.Select(x => (string)x["InterfaceType"] == "Rest" ? (BaseInterface)x.ToObject<RestInterface>()
: (BaseInterface)x.ToObject<ModbusTcpInterface>()).ToList();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
}
but if you have an access to code that creates a json string, you can change it by using TypeNameHandling setting
var jsonSerializerSettings = new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.All
};
var json = JsonConvert.SerializeObject(deviceConfiguration, jsonSerializerSettings);
In this case it will be much more simple code to deserialize
deviceConfiguration=JsonConvert.DeserializeObject<DeviceConfiguration>(json, jsonSerializerSettings);
You should be using JArray instead of JObject since SupportedInterfaces is an array not an object as the error says.
SupportedInterfaces looks like this in your JsonReader
[
{
"Name": "My first interface",
"InterfaceType": "Rest",
"Option": "option string here..",
"DefaultBaseUri": "mybaseurl.io/itsme",
},
{
"Name": "My second interface",
"InterfaceType": "ModbusTcp",
"Option": "option string here..",
"IpAddress": "127.0.0.1",
"Port": 502
},
{
"Name": "My third interface",
"InterfaceType": "Rest",
"Option": "option string here..",
"DefaultBaseUri": "base.url/api/devices",
},
]
Related
This Imgur api call returns a list containing both Gallery Image and Gallery Album classes represented in JSON.
I can't see how to deserialize these automatically using Json.NET given that there is no $type property telling the deserializer which class is meant to be represented. There is a property called "IsAlbum" that can be used to differentiate between the two.
This question appears to show one method but it looks like a bit of a hack.
How do I go about deserializing these classes? (using C#, Json.NET).
Sample Data:
Gallery Image
{
"id": "OUHDm",
"title": "My most recent drawing. Spent over 100 hours.",
...
"is_album": false
}
Gallery Album
{
"id": "lDRB2",
"title": "Imgur Office",
...
"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"
}
]
}
}
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
Simply with JsonSubTypes attributes that work with Json.NET
[JsonConverter(typeof(JsonSubtypes), "is_album")]
[JsonSubtypes.KnownSubType(typeof(GalleryAlbum), true)]
[JsonSubtypes.KnownSubType(typeof(GalleryImage), false)]
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; }
}
Advanced to Brian Rogers answer. And about "use Serializer.Populate() instead of item.ToObject()".
If derived types has contstructors or some of their has own customconverter you must use general way for deserialize JSON.
So you must leave work for instantiate new object to NewtonJson. This way you can achieve it in you CustomJsonConverter:
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
..... YOU Code For Determine Real Type of Json Record .......
// 1. Correct ContractResolver for you derived type
var contract = serializer.ContractResolver.ResolveContract(DeterminedType);
if (converter != null && !typeDeserializer.Type.IsAbstract && converter.GetType() == GetType())
{
contract.Converter = null; // Clean Wrong Converter grabbed by DefaultContractResolver from you base class for derived class
}
// Deserialize in general way
var jTokenReader = new JTokenReader(jObject);
var result = serializer.Deserialize(jTokenReader, DeterminedType);
return (result);
}
This work if you have recursion of objects.
I'm only posting this to clear up some of the confusion. If you are working with a predefined format and need to deserialize it, this is what I found worked best and demonstrates the mechanics so that others can tweak it as needed.
public class BaseClassConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var j = JObject.Load(reader);
var retval = BaseClass.From(j, serializer);
return retval;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
public override bool CanConvert(Type objectType)
{
// important - do not cause subclasses to go through this converter
return objectType == typeof(BaseClass);
}
}
// important to not use attribute otherwise you'll infinite loop
public abstract class BaseClass
{
internal static Type[] Types = new Type[] {
typeof(Subclass1),
typeof(Subclass2),
typeof(Subclass3)
};
internal static Dictionary<string, Type> TypesByName = Types.ToDictionary(t => t.Name.Split('.').Last());
// type property based off of class name
[JsonProperty(PropertyName = "type", Required = Required.Always)]
public string JsonObjectType { get { return this.GetType().Name.Split('.').Last(); } set { } }
// convenience method to deserialize a JObject
public static new BaseClass From(JObject obj, JsonSerializer serializer)
{
// this is our object type property
var str = (string)obj["type"];
// we map using a dictionary, but you can do whatever you want
var type = TypesByName[str];
// important to pass serializer (and its settings) along
return obj.ToObject(type, serializer) as BaseClass;
}
// convenience method for deserialization
public static BaseClass Deserialize(JsonReader reader)
{
JsonSerializer ser = new JsonSerializer();
// important to add converter here
ser.Converters.Add(new BaseClassConverter());
return ser.Deserialize<BaseClass>(reader);
}
}
Following implementation should let you de-serialize without changing the way you have designed your classes and by using a field other than $type to decide what to de-serialize it into.
public class GalleryImageConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(GalleryImage) || objectType == typeof(GalleryAlbum));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
if (!CanConvert(objectType))
throw new InvalidDataException("Invalid type of object");
JObject jo = JObject.Load(reader);
// following is to avoid use of magic strings
var isAlbumPropertyName = ((MemberExpression)((Expression<Func<GalleryImage, bool>>)(s => s.is_album)).Body).Member.Name;
JToken jt;
if (!jo.TryGetValue(isAlbumPropertyName, StringComparison.InvariantCultureIgnoreCase, out jt))
{
return jo.ToObject<GalleryImage>();
}
var propValue = jt.Value<bool>();
if(propValue) {
resultType = typeof(GalleryAlbum);
}
else{
resultType = typeof(GalleryImage);
}
var resultObject = Convert.ChangeType(Activator.CreateInstance(resultType), resultType);
var objectProperties=resultType.GetProperties();
foreach (var objectProperty in objectProperties)
{
var propType = objectProperty.PropertyType;
var propName = objectProperty.Name;
var token = jo.GetValue(propName, StringComparison.InvariantCultureIgnoreCase);
if (token != null)
{
objectProperty.SetValue(resultObject,token.ToObject(propType)?? objectProperty.GetValue(resultObject));
}
}
return resultObject;
}
catch (Exception ex)
{
throw;
}
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
#ИгорьОрлов's answer works for when you have types that can only be instantiated directly by JSON.net (due to [JsonConstructor] and/or use of [JsonProperty] directly on constructor parameters. However overwriting contract.Converter = null does not work when JSON.net has already cached the converter to use.
(This wouldn't be an issue if JSON.NET used immutable types to indicate when data and configuration is no-longer mutable, le sigh)
In my case, I did this:
Implemented a custom JsonConverter<T> (where T is my DTO's base class).
Defined a DefaultContractResolver subclass that overrides ResolveContractConverter to return my custom JsonConverter for only the base class.
In detail, and by example:
Supposing I have these immutable DTOs that represent a remote file-system (so there' DirectoryDto and FileDto which both inherit FileSystemDto, just like how DirectoryInfo and FileInfo derive from System.IO.FileSystemInfo):
public enum DtoKind
{
None = 0,
File,
Directory
}
public abstract class FileSystemDto
{
protected FileSystemDto( String name, DtoKind kind )
{
this.Name = name ?? throw new ArgumentNullException(nameof(name));
this.Kind = kind;
}
[JsonProperty( "name" )]
public String Name { get; }
[JsonProperty( "kind" )]
public String Kind { get; }
}
public class FileDto : FileSystemDto
{
[JsonConstructor]
public FileDto(
[JsonProperty("name" )] String name,
[JsonProperty("length")] Int64 length,
[JsonProperty("kind") ] DtoKind kind
)
: base( name: name, kind: kind )
{
if( kind != DtoKind.File ) throw new InvalidOperationException( "blargh" );
this.Length = length;
}
[JsonProperty( "length" )]
public Int64 Length { get; }
}
public class DirectoryDto : FileSystemDto
{
[JsonConstructor]
public FileDto(
[JsonProperty("name")] String name,
[JsonProperty("kind")] DtoKind kind
)
: base( name: name, kind: kind )
{
if( kind != DtoKind.Directory ) throw new InvalidOperationException( "blargh" );
}
}
Supposing I have a JSON array of FileSystemDto:
[
{ "name": "foo.txt", "kind": "File", "length": 12345 },
{ "name": "bar.txt", "kind": "File", "length": 12345 },
{ "name": "subdir", "kind": "Directory" },
]
I want Json.net to deserialize this to List<FileSystemDto>...
So define a subclass of DefaultContractResolver (or if you already have a resolver implementation then subclass (or compose) that) and override ResolveContractConverter:
public class MyContractResolver : DefaultContractResolver
{
protected override JsonConverter? ResolveContractConverter( Type objectType )
{
if( objectType == typeof(FileSystemDto) )
{
return MyJsonConverter.Instance;
}
else if( objectType == typeof(FileDto ) )
{
// use default
}
else if( objectType == typeof(DirectoryDto) )
{
// use default
}
return base.ResolveContractConverter( objectType );
}
}
Then implement MyJsonConverter:
public class MyJsonConverter : JsonConverter<FileSystemDto>
{
public static MyJsonConverter Instance { get; } = new MyJsonConverter();
private MyJsonConverter() {}
// TODO: Override `CanWrite => false` and `WriteJson { throw; }` if you like.
public override FileSystemDto? ReadJson( JsonReader reader, Type objectType, FileSystemDto? existingValue, Boolean hasExistingValue, JsonSerializer serializer )
{
if( reader.TokenType == JsonToken.Null ) return null;
if( objectType == typeof(FileSystemDto) )
{
JObject jsonObject = JObject.Load( reader );
if( jsonObject.Property( "kind" )?.Value is JValue jv && jv.Value is String kind )
{
if( kind == "File" )
{
return jsonObject.ToObject<FileDto>( serializer );
}
else if( kind == "Directory" )
{
return jsonObject.ToObject<DirectoryDto>( serializer );
}
}
}
return null; // or throw, depending on your strictness.
}
}
Then, to deserialize, use a JsonSerializer instance with the ContractResolver set correctly, for example:
public static IReadOnlyList<FileSystemDto> DeserializeFileSystemJsonArray( String json )
{
JsonSerializer jss = new JsonSerializer()
{
ContractResolver = new KuduDtoContractResolver()
};
using( StringReader strRdr = new StringReader( json ) )
using( JsonTextReader jsonRdr = new JsonTextReader( strRdr ) )
{
List<FileSystemDto>? list = jss.Deserialize< List<FileSystemDto> >( jsonRdr );
// TODO: Throw if `list` is null.
return list;
}
}
I'm using Newtonsoft JSON to serialization and deserialization.
I have json representing fields where title property is representing a display Name:
{
"ID": {
"type": "integer",
"title": "ID Display Name"
},
"TITLE": {
"type": "string",
"title": "Title Display name"
},
"NAME": {
"type": "string",
"title": "Name"
},
"CUSTOM_123": {
"type": "string",
"title": "CUSTOM_123",
"formLabel": "CUSTOM_123 Display Name"
}
// ... etc
}
And I have class:
public class Field
{
public string Title { get; set; }
public string Type { get; set; }
}
Now everything working fine. But when field name starts with "CUSTOM_" I need to serialize "Title" from property "formLabel".
How could I implement conditional property source?
You can parse your JSON to JObject, then enumerate all properties, deserialize them one by one to Field instance and update a Title value if property name in JSON starts with CUSTOM_ string
var json = JObject.Parse(jsonString);
foreach (var property in json.Properties())
{
var field = property.Value.ToObject<Field>();
if (property.Name.StartsWith("CUSTOM_")&&property.Value is JObject propertyObject)
{
field.Title = propertyObject["formLabel"]?.Value<string>();
}
}
This approach is simpler, rather than writing a custom converter, I believe
having your custom JsonConverter will do the trick,so you will separate the business of deserialization and you can include more business as required in the Convert Method. so in first lines newton soft serializes normally our object to Field then to have the custom business for "Custom_",working fiddle https://dotnetfiddle.net/aVpB8I
Code as below :
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
namespace testapp
{
class Program
{
static void Main(string[] args)
{
string json = #"
{
""ID"": {
""type"": ""integer"",
""title"": ""ID Display Name""
},
""TITLE"": {
""type"": ""string"",
""title"": ""Title Display name""
},
""NAME"": {
""type"": ""string"",
""title"": ""Name""
},
""CUSTOM_123"": {
""type"": ""string"",
""title"": ""CUSTOM_123"",
""formLabel"": ""CUSTOM_123 Display Name""
}
}";
List<Field> result = JsonConvert.DeserializeObject<List<Field>>(json, new JobInfoConverter());
}
public class Field
{
public string title { get; set; }
public string type { get; set; }
}
public class JobInfoConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
List<Field> result = new List<Field>();
var content = JObject.Load(reader);
foreach (var prop in content.Properties())
{
var parsedValue = prop.Value.ToObject<Field>();
if (prop.Name.StartsWith("CUSTOM_"))
{
parsedValue.title = prop.Value["formLabel"].ToString();
}
result.Add(parsedValue);
}
return result;
}
catch (Exception ex)
{
return null;
}
}
public override void WriteJson(JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) => throw new NotImplementedException();
public override bool CanConvert(Type objectType)
{
return true;
}
public override bool CanWrite => true;
}
}
}
You can deserialize to object using Newtonsoft with custom JsonConverter.
Your JSON structure, is a JSON object, that contains multiple child JSON objects (fields). So when deserializing it to .NET you could create a separate class for parent that will have a list of Fields. You could also change you JSON to array instead of object and deserialize to List<Fields>.
IList<Field> fields = JsonConvert.DeserializeObject<IList<Field>>(json, new CustomJsonConverter());
public class CustomJsonConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
IList<Field> fields = new List<Field>();
JObject obj = JObject.Load(reader);
foreach (JToken child in obj.Children())
{
Field field = new Field();
JProperty fieldProp = (JProperty)child;
JObject fieldValue = (JObject)fieldProp.Value;
string fieldName = fieldProp.Name;
if (fieldName .StartsWith("CUSTOM_"))
{
field.Title = (string)fieldValue["formLabel"];
field.Type = (string)fieldValue["type"];
}
else
{
field.Title = (string)fieldValue["title"];
field.Type = (string)fieldValue["type"];
}
fields.Add(field);
}
return fields;
}
public override bool CanConvert(Type objectType)
{
return true;
}
}
public class Field
{
public string Title { get; set; }
public string Type{ get; set; }
}
https://dotnetfiddle.net/5bNihC
I have the following class...
public class ResultDTO
{
public string MSAccountNumber { get; set; }
public string MSCaseNumber { get; set; }
public string StatusMessage { get; set; }
public string StatCode { get; set; }
}
Unsurprisingly, JsonConvert.DeserializeObject is unable to convert the following JSON into that object....
{
"StatusMessage": [
"Record processed successfully"
],
"StatCode": [
"200"
],
"MSCaseNumber": [
"500"
],
"MSAccountNumber": [
"001"
]
}
I have asked the API creator if he can change the API so that the returned JSON has string properties instead of List<string> properties. But in case he is unable to accommodate that, how do I implement and use a JsonConverter so that JsonConvert will successfully convert that JSON into my target class. Assume that all of the List properties in the JSON will have at least one item (is never null or empty) and we want to take the first item.
I know this can be done but I have no idea how.
Here is a converter that should work for you:
class StringArrayToStringConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(string);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null) return null;
if (reader.TokenType == JsonToken.String) return (string)reader.Value;
if (reader.TokenType == JsonToken.StartArray)
{
JArray array = JArray.Load(reader);
string value = (string)array.Children<JValue>().FirstOrDefault();
return value;
}
throw new JsonException("Unexpected token type: " + reader.TokenType);
}
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Use it like this:
var dto = JsonConvert.DeserializeObject<ResultDTO>(json, new StringArrayToStringConverter());
Working demo: https://dotnetfiddle.net/kLbn2P
This Imgur api call returns a list containing both Gallery Image and Gallery Album classes represented in JSON.
I can't see how to deserialize these automatically using Json.NET given that there is no $type property telling the deserializer which class is meant to be represented. There is a property called "IsAlbum" that can be used to differentiate between the two.
This question appears to show one method but it looks like a bit of a hack.
How do I go about deserializing these classes? (using C#, Json.NET).
Sample Data:
Gallery Image
{
"id": "OUHDm",
"title": "My most recent drawing. Spent over 100 hours.",
...
"is_album": false
}
Gallery Album
{
"id": "lDRB2",
"title": "Imgur Office",
...
"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"
}
]
}
}
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
Simply with JsonSubTypes attributes that work with Json.NET
[JsonConverter(typeof(JsonSubtypes), "is_album")]
[JsonSubtypes.KnownSubType(typeof(GalleryAlbum), true)]
[JsonSubtypes.KnownSubType(typeof(GalleryImage), false)]
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; }
}
Advanced to Brian Rogers answer. And about "use Serializer.Populate() instead of item.ToObject()".
If derived types has contstructors or some of their has own customconverter you must use general way for deserialize JSON.
So you must leave work for instantiate new object to NewtonJson. This way you can achieve it in you CustomJsonConverter:
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
..... YOU Code For Determine Real Type of Json Record .......
// 1. Correct ContractResolver for you derived type
var contract = serializer.ContractResolver.ResolveContract(DeterminedType);
if (converter != null && !typeDeserializer.Type.IsAbstract && converter.GetType() == GetType())
{
contract.Converter = null; // Clean Wrong Converter grabbed by DefaultContractResolver from you base class for derived class
}
// Deserialize in general way
var jTokenReader = new JTokenReader(jObject);
var result = serializer.Deserialize(jTokenReader, DeterminedType);
return (result);
}
This work if you have recursion of objects.
I'm only posting this to clear up some of the confusion. If you are working with a predefined format and need to deserialize it, this is what I found worked best and demonstrates the mechanics so that others can tweak it as needed.
public class BaseClassConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var j = JObject.Load(reader);
var retval = BaseClass.From(j, serializer);
return retval;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
public override bool CanConvert(Type objectType)
{
// important - do not cause subclasses to go through this converter
return objectType == typeof(BaseClass);
}
}
// important to not use attribute otherwise you'll infinite loop
public abstract class BaseClass
{
internal static Type[] Types = new Type[] {
typeof(Subclass1),
typeof(Subclass2),
typeof(Subclass3)
};
internal static Dictionary<string, Type> TypesByName = Types.ToDictionary(t => t.Name.Split('.').Last());
// type property based off of class name
[JsonProperty(PropertyName = "type", Required = Required.Always)]
public string JsonObjectType { get { return this.GetType().Name.Split('.').Last(); } set { } }
// convenience method to deserialize a JObject
public static new BaseClass From(JObject obj, JsonSerializer serializer)
{
// this is our object type property
var str = (string)obj["type"];
// we map using a dictionary, but you can do whatever you want
var type = TypesByName[str];
// important to pass serializer (and its settings) along
return obj.ToObject(type, serializer) as BaseClass;
}
// convenience method for deserialization
public static BaseClass Deserialize(JsonReader reader)
{
JsonSerializer ser = new JsonSerializer();
// important to add converter here
ser.Converters.Add(new BaseClassConverter());
return ser.Deserialize<BaseClass>(reader);
}
}
Following implementation should let you de-serialize without changing the way you have designed your classes and by using a field other than $type to decide what to de-serialize it into.
public class GalleryImageConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(GalleryImage) || objectType == typeof(GalleryAlbum));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
if (!CanConvert(objectType))
throw new InvalidDataException("Invalid type of object");
JObject jo = JObject.Load(reader);
// following is to avoid use of magic strings
var isAlbumPropertyName = ((MemberExpression)((Expression<Func<GalleryImage, bool>>)(s => s.is_album)).Body).Member.Name;
JToken jt;
if (!jo.TryGetValue(isAlbumPropertyName, StringComparison.InvariantCultureIgnoreCase, out jt))
{
return jo.ToObject<GalleryImage>();
}
var propValue = jt.Value<bool>();
if(propValue) {
resultType = typeof(GalleryAlbum);
}
else{
resultType = typeof(GalleryImage);
}
var resultObject = Convert.ChangeType(Activator.CreateInstance(resultType), resultType);
var objectProperties=resultType.GetProperties();
foreach (var objectProperty in objectProperties)
{
var propType = objectProperty.PropertyType;
var propName = objectProperty.Name;
var token = jo.GetValue(propName, StringComparison.InvariantCultureIgnoreCase);
if (token != null)
{
objectProperty.SetValue(resultObject,token.ToObject(propType)?? objectProperty.GetValue(resultObject));
}
}
return resultObject;
}
catch (Exception ex)
{
throw;
}
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
#ИгорьОрлов's answer works for when you have types that can only be instantiated directly by JSON.net (due to [JsonConstructor] and/or use of [JsonProperty] directly on constructor parameters. However overwriting contract.Converter = null does not work when JSON.net has already cached the converter to use.
(This wouldn't be an issue if JSON.NET used immutable types to indicate when data and configuration is no-longer mutable, le sigh)
In my case, I did this:
Implemented a custom JsonConverter<T> (where T is my DTO's base class).
Defined a DefaultContractResolver subclass that overrides ResolveContractConverter to return my custom JsonConverter for only the base class.
In detail, and by example:
Supposing I have these immutable DTOs that represent a remote file-system (so there' DirectoryDto and FileDto which both inherit FileSystemDto, just like how DirectoryInfo and FileInfo derive from System.IO.FileSystemInfo):
public enum DtoKind
{
None = 0,
File,
Directory
}
public abstract class FileSystemDto
{
protected FileSystemDto( String name, DtoKind kind )
{
this.Name = name ?? throw new ArgumentNullException(nameof(name));
this.Kind = kind;
}
[JsonProperty( "name" )]
public String Name { get; }
[JsonProperty( "kind" )]
public String Kind { get; }
}
public class FileDto : FileSystemDto
{
[JsonConstructor]
public FileDto(
[JsonProperty("name" )] String name,
[JsonProperty("length")] Int64 length,
[JsonProperty("kind") ] DtoKind kind
)
: base( name: name, kind: kind )
{
if( kind != DtoKind.File ) throw new InvalidOperationException( "blargh" );
this.Length = length;
}
[JsonProperty( "length" )]
public Int64 Length { get; }
}
public class DirectoryDto : FileSystemDto
{
[JsonConstructor]
public FileDto(
[JsonProperty("name")] String name,
[JsonProperty("kind")] DtoKind kind
)
: base( name: name, kind: kind )
{
if( kind != DtoKind.Directory ) throw new InvalidOperationException( "blargh" );
}
}
Supposing I have a JSON array of FileSystemDto:
[
{ "name": "foo.txt", "kind": "File", "length": 12345 },
{ "name": "bar.txt", "kind": "File", "length": 12345 },
{ "name": "subdir", "kind": "Directory" },
]
I want Json.net to deserialize this to List<FileSystemDto>...
So define a subclass of DefaultContractResolver (or if you already have a resolver implementation then subclass (or compose) that) and override ResolveContractConverter:
public class MyContractResolver : DefaultContractResolver
{
protected override JsonConverter? ResolveContractConverter( Type objectType )
{
if( objectType == typeof(FileSystemDto) )
{
return MyJsonConverter.Instance;
}
else if( objectType == typeof(FileDto ) )
{
// use default
}
else if( objectType == typeof(DirectoryDto) )
{
// use default
}
return base.ResolveContractConverter( objectType );
}
}
Then implement MyJsonConverter:
public class MyJsonConverter : JsonConverter<FileSystemDto>
{
public static MyJsonConverter Instance { get; } = new MyJsonConverter();
private MyJsonConverter() {}
// TODO: Override `CanWrite => false` and `WriteJson { throw; }` if you like.
public override FileSystemDto? ReadJson( JsonReader reader, Type objectType, FileSystemDto? existingValue, Boolean hasExistingValue, JsonSerializer serializer )
{
if( reader.TokenType == JsonToken.Null ) return null;
if( objectType == typeof(FileSystemDto) )
{
JObject jsonObject = JObject.Load( reader );
if( jsonObject.Property( "kind" )?.Value is JValue jv && jv.Value is String kind )
{
if( kind == "File" )
{
return jsonObject.ToObject<FileDto>( serializer );
}
else if( kind == "Directory" )
{
return jsonObject.ToObject<DirectoryDto>( serializer );
}
}
}
return null; // or throw, depending on your strictness.
}
}
Then, to deserialize, use a JsonSerializer instance with the ContractResolver set correctly, for example:
public static IReadOnlyList<FileSystemDto> DeserializeFileSystemJsonArray( String json )
{
JsonSerializer jss = new JsonSerializer()
{
ContractResolver = new KuduDtoContractResolver()
};
using( StringReader strRdr = new StringReader( json ) )
using( JsonTextReader jsonRdr = new JsonTextReader( strRdr ) )
{
List<FileSystemDto>? list = jss.Deserialize< List<FileSystemDto> >( jsonRdr );
// TODO: Throw if `list` is null.
return list;
}
}
This Imgur api call returns a list containing both Gallery Image and Gallery Album classes represented in JSON.
I can't see how to deserialize these automatically using Json.NET given that there is no $type property telling the deserializer which class is meant to be represented. There is a property called "IsAlbum" that can be used to differentiate between the two.
This question appears to show one method but it looks like a bit of a hack.
How do I go about deserializing these classes? (using C#, Json.NET).
Sample Data:
Gallery Image
{
"id": "OUHDm",
"title": "My most recent drawing. Spent over 100 hours.",
...
"is_album": false
}
Gallery Album
{
"id": "lDRB2",
"title": "Imgur Office",
...
"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"
}
]
}
}
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
Simply with JsonSubTypes attributes that work with Json.NET
[JsonConverter(typeof(JsonSubtypes), "is_album")]
[JsonSubtypes.KnownSubType(typeof(GalleryAlbum), true)]
[JsonSubtypes.KnownSubType(typeof(GalleryImage), false)]
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; }
}
Advanced to Brian Rogers answer. And about "use Serializer.Populate() instead of item.ToObject()".
If derived types has contstructors or some of their has own customconverter you must use general way for deserialize JSON.
So you must leave work for instantiate new object to NewtonJson. This way you can achieve it in you CustomJsonConverter:
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
..... YOU Code For Determine Real Type of Json Record .......
// 1. Correct ContractResolver for you derived type
var contract = serializer.ContractResolver.ResolveContract(DeterminedType);
if (converter != null && !typeDeserializer.Type.IsAbstract && converter.GetType() == GetType())
{
contract.Converter = null; // Clean Wrong Converter grabbed by DefaultContractResolver from you base class for derived class
}
// Deserialize in general way
var jTokenReader = new JTokenReader(jObject);
var result = serializer.Deserialize(jTokenReader, DeterminedType);
return (result);
}
This work if you have recursion of objects.
I'm only posting this to clear up some of the confusion. If you are working with a predefined format and need to deserialize it, this is what I found worked best and demonstrates the mechanics so that others can tweak it as needed.
public class BaseClassConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var j = JObject.Load(reader);
var retval = BaseClass.From(j, serializer);
return retval;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
public override bool CanConvert(Type objectType)
{
// important - do not cause subclasses to go through this converter
return objectType == typeof(BaseClass);
}
}
// important to not use attribute otherwise you'll infinite loop
public abstract class BaseClass
{
internal static Type[] Types = new Type[] {
typeof(Subclass1),
typeof(Subclass2),
typeof(Subclass3)
};
internal static Dictionary<string, Type> TypesByName = Types.ToDictionary(t => t.Name.Split('.').Last());
// type property based off of class name
[JsonProperty(PropertyName = "type", Required = Required.Always)]
public string JsonObjectType { get { return this.GetType().Name.Split('.').Last(); } set { } }
// convenience method to deserialize a JObject
public static new BaseClass From(JObject obj, JsonSerializer serializer)
{
// this is our object type property
var str = (string)obj["type"];
// we map using a dictionary, but you can do whatever you want
var type = TypesByName[str];
// important to pass serializer (and its settings) along
return obj.ToObject(type, serializer) as BaseClass;
}
// convenience method for deserialization
public static BaseClass Deserialize(JsonReader reader)
{
JsonSerializer ser = new JsonSerializer();
// important to add converter here
ser.Converters.Add(new BaseClassConverter());
return ser.Deserialize<BaseClass>(reader);
}
}
Following implementation should let you de-serialize without changing the way you have designed your classes and by using a field other than $type to decide what to de-serialize it into.
public class GalleryImageConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(GalleryImage) || objectType == typeof(GalleryAlbum));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
if (!CanConvert(objectType))
throw new InvalidDataException("Invalid type of object");
JObject jo = JObject.Load(reader);
// following is to avoid use of magic strings
var isAlbumPropertyName = ((MemberExpression)((Expression<Func<GalleryImage, bool>>)(s => s.is_album)).Body).Member.Name;
JToken jt;
if (!jo.TryGetValue(isAlbumPropertyName, StringComparison.InvariantCultureIgnoreCase, out jt))
{
return jo.ToObject<GalleryImage>();
}
var propValue = jt.Value<bool>();
if(propValue) {
resultType = typeof(GalleryAlbum);
}
else{
resultType = typeof(GalleryImage);
}
var resultObject = Convert.ChangeType(Activator.CreateInstance(resultType), resultType);
var objectProperties=resultType.GetProperties();
foreach (var objectProperty in objectProperties)
{
var propType = objectProperty.PropertyType;
var propName = objectProperty.Name;
var token = jo.GetValue(propName, StringComparison.InvariantCultureIgnoreCase);
if (token != null)
{
objectProperty.SetValue(resultObject,token.ToObject(propType)?? objectProperty.GetValue(resultObject));
}
}
return resultObject;
}
catch (Exception ex)
{
throw;
}
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
#ИгорьОрлов's answer works for when you have types that can only be instantiated directly by JSON.net (due to [JsonConstructor] and/or use of [JsonProperty] directly on constructor parameters. However overwriting contract.Converter = null does not work when JSON.net has already cached the converter to use.
(This wouldn't be an issue if JSON.NET used immutable types to indicate when data and configuration is no-longer mutable, le sigh)
In my case, I did this:
Implemented a custom JsonConverter<T> (where T is my DTO's base class).
Defined a DefaultContractResolver subclass that overrides ResolveContractConverter to return my custom JsonConverter for only the base class.
In detail, and by example:
Supposing I have these immutable DTOs that represent a remote file-system (so there' DirectoryDto and FileDto which both inherit FileSystemDto, just like how DirectoryInfo and FileInfo derive from System.IO.FileSystemInfo):
public enum DtoKind
{
None = 0,
File,
Directory
}
public abstract class FileSystemDto
{
protected FileSystemDto( String name, DtoKind kind )
{
this.Name = name ?? throw new ArgumentNullException(nameof(name));
this.Kind = kind;
}
[JsonProperty( "name" )]
public String Name { get; }
[JsonProperty( "kind" )]
public String Kind { get; }
}
public class FileDto : FileSystemDto
{
[JsonConstructor]
public FileDto(
[JsonProperty("name" )] String name,
[JsonProperty("length")] Int64 length,
[JsonProperty("kind") ] DtoKind kind
)
: base( name: name, kind: kind )
{
if( kind != DtoKind.File ) throw new InvalidOperationException( "blargh" );
this.Length = length;
}
[JsonProperty( "length" )]
public Int64 Length { get; }
}
public class DirectoryDto : FileSystemDto
{
[JsonConstructor]
public FileDto(
[JsonProperty("name")] String name,
[JsonProperty("kind")] DtoKind kind
)
: base( name: name, kind: kind )
{
if( kind != DtoKind.Directory ) throw new InvalidOperationException( "blargh" );
}
}
Supposing I have a JSON array of FileSystemDto:
[
{ "name": "foo.txt", "kind": "File", "length": 12345 },
{ "name": "bar.txt", "kind": "File", "length": 12345 },
{ "name": "subdir", "kind": "Directory" },
]
I want Json.net to deserialize this to List<FileSystemDto>...
So define a subclass of DefaultContractResolver (or if you already have a resolver implementation then subclass (or compose) that) and override ResolveContractConverter:
public class MyContractResolver : DefaultContractResolver
{
protected override JsonConverter? ResolveContractConverter( Type objectType )
{
if( objectType == typeof(FileSystemDto) )
{
return MyJsonConverter.Instance;
}
else if( objectType == typeof(FileDto ) )
{
// use default
}
else if( objectType == typeof(DirectoryDto) )
{
// use default
}
return base.ResolveContractConverter( objectType );
}
}
Then implement MyJsonConverter:
public class MyJsonConverter : JsonConverter<FileSystemDto>
{
public static MyJsonConverter Instance { get; } = new MyJsonConverter();
private MyJsonConverter() {}
// TODO: Override `CanWrite => false` and `WriteJson { throw; }` if you like.
public override FileSystemDto? ReadJson( JsonReader reader, Type objectType, FileSystemDto? existingValue, Boolean hasExistingValue, JsonSerializer serializer )
{
if( reader.TokenType == JsonToken.Null ) return null;
if( objectType == typeof(FileSystemDto) )
{
JObject jsonObject = JObject.Load( reader );
if( jsonObject.Property( "kind" )?.Value is JValue jv && jv.Value is String kind )
{
if( kind == "File" )
{
return jsonObject.ToObject<FileDto>( serializer );
}
else if( kind == "Directory" )
{
return jsonObject.ToObject<DirectoryDto>( serializer );
}
}
}
return null; // or throw, depending on your strictness.
}
}
Then, to deserialize, use a JsonSerializer instance with the ContractResolver set correctly, for example:
public static IReadOnlyList<FileSystemDto> DeserializeFileSystemJsonArray( String json )
{
JsonSerializer jss = new JsonSerializer()
{
ContractResolver = new KuduDtoContractResolver()
};
using( StringReader strRdr = new StringReader( json ) )
using( JsonTextReader jsonRdr = new JsonTextReader( strRdr ) )
{
List<FileSystemDto>? list = jss.Deserialize< List<FileSystemDto> >( jsonRdr );
// TODO: Throw if `list` is null.
return list;
}
}