Deserialize nested interfaces - c#

Reproduction can be seen here: Gist.
I'm receiving an error when attempting to deserialize a collection of interfaces - particularly where the interface also contains a collection of the same interfaces. If I remove the nested collection, it deserializes fine. I had thought that implementing the JsonConverter would handle this, but I seem to be misunderstanding something.
Note that I do not have control over the serialization.
The error I'm receiving is:
Could not create an instance of type JsonRepro.IMember. Type is an interface or abstract class and cannot be instantiated. Path 'members[0].id', line 17, position 25.
Minimal reproduction is as follows:
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace JsonRepro
{
public interface IMember
{
int ID { get; set; }
string Type { get; set; }
}
public class A : IMember
{
public int ID { get; set; }
public string Type { get; set; }
public List<IMember> Members { get; set; }
}
public class B : IMember
{
public int ID { get; set; }
public string Name { get; set; }
public string Type { get; set; }
}
public class MyConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(IMember).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader,
Type objectType, object existingValue, JsonSerializer serializer)
{
JObject item = JObject.Load(reader);
var typeValue = item["type"].Value<string>();
switch (typeValue)
{
case "A":
return item.ToObject<A>();
case "B":
return item.ToObject<B>();
}
throw new NotSupportedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override bool CanWrite => false;
}
class Program
{
private const string json = #"
[
{
'id': 0,
'type': 'A'
},
{
'id': 1,
'type': 'B',
'name': 'One'
},
{
'id': 2,
'type': 'A',
'members': [
{
'id': 3,
'type': 'A'
}
]
}
]
";
static void Main(string[] args)
{
var settings = new JsonSerializerSettings() { Converters = { new MyConverter() } };
var result = JsonConvert.DeserializeObject<List<IMember>>(json, settings);
Console.WriteLine(result);
Console.ReadKey();
}
}
}

If you are in control of both the serialization and the deserialization, try using TypeNameHandling.All in your serializer settings on both serialize and deserialize.
When you have an abstract type like an interface, it doesn't know which concrete type to use. The outer concrete type can be specified in the Deserialize type parameter, but the inner types can't. Using TypeNameHandling lets you deserialize to an abstract type, but it will actually deserialize to the concrete type specified in $type.
The alternative is to deserialize to JObjects and then individually ToObject<T> on each of those, but then you have to do it on every inner object.
You can also use a custom JsonConverter but that's even more complexity.
Another alternative is to just not use interfaces for storing data if you can avoid it. If it's just a bunch of properties with no methods, it probably doesn't need an interface.
Update:
How about this modification to your custom JsonConverter:
case "A":
return new A
{
ID = item["id"]?.Value<int>() ?? 0,
Type = typeValue,
Members = ((JArray)item["members"])?.ToObject<List<IMember>>(serializer)
};
It's not pretty, but it works. You need to pass the serializer with your custom converter through on ToObject but if you do item.ToObject<A>(serializer) you get a stack overflow, so you need to only do it on the members property.

Related

Upgrading JsonApiSerializer from 1.3.1 to 1.74 Issues with custom converter

I'm using this git repository JsonApiSerializer with a project and tying to update to the latest version 1.74. Everything works fine in version 1.3.1 but after upgrading to version 1.74 the custom converter cant get the Json to deserialize properly. I set a breakpoint on the converter put it never hits the ReadJson Method, instead it jumps out on the line
public override bool CanConvert(Type objectType) => false;
Is anyone familiar with JsonApiSerializer or customer converters able to see what the issue might be.
I receive this error.
JsonSerializationException: Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.String' because the type requires a JSON primitive value (e.g. string, number, boolean, null) to deserialize correctly. To fix this error either change the JSON to a JSON primitive value (e.g. string, number, boolean, null) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List<T>) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object. Path 'data[0].relationships.matches.data[0].type'.
Object I'm trying to deserialize to
public class PubgPlayer : PubgShardedEntity
{
[JsonProperty("Name")]
public string Name { get; set; }
[JsonProperty]
public DateTime CreatedAt { get; set; }
[JsonProperty]
public string PatchVersion { get; set; }
[JsonProperty]
public string TitleId { get; set; }
[JsonProperty("matches")]
[JsonConverter(typeof(RelationshipIdConverter))]
public IEnumerable<string> MatchIds { get; set; }
}
This is the custom converter
public class RelationshipIdConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => false;
public override bool CanWrite => false;
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
//if the reader is not reading a relationship object just deserialize as normal.
//This allows us to serialize and deserialize multiple times after converting from the Json-API format
if (reader.TokenType != JsonToken.StartObject)
return serializer.Deserialize(reader, objectType);
JToken jt = JToken.Load(reader);
var dataToken = jt.SelectToken("data");
if (objectType == typeof(string))
return dataToken["id"].ToString();
return dataToken.Select(x => (string)x["id"]).ToList();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { }
}
Example Json
{
"data":[
{
"type":"player",
"id":"account.f96f1d482a5d4a21a4135f598f5a5c6a",
"attributes":{
"name":"Nomad_o7",
"stats":null,
"titleId":"pubg",
"shardId":"steam",
"patchVersion":""
},
"relationships":{
"assets":{
"data":[
]
},
"matches":{
"data":[
{
"type":"match",
"id":"e14ca749-3c19-49dd-9486-03de90669fa3"
},
{
"type":"match",
"id":"73dbca3e-eeee-44eb-9c43-3f71df0e02ae"
},
{
"type":"match",
"id":"e9f80ed0-1e78-4500-950f-174028fb73cc"
}
]
}
},
"links":{
"self":"https://api.pubg.com/shards/steam/players/account.f96f1d482a5d4a21a4135f598f5a5c6a",
"schema":""
}
},
{
"type":"player",
"id":"account.82bad0072f31455d8d9f8d834da2f2f3",
"attributes":{
"name":"TGLTN",
"stats":null,
"titleId":"pubg",
"shardId":"steam",
"patchVersion":""
},
"relationships":{
"assets":{
"data":[
]
},
"matches":{
"data":[
{
"type":"match",
"id":"4a6f064e-c292-4e9b-a3a6-bbaed891ce27"
},
{
"type":"match",
"id":"efd9434c-4116-4b56-815d-e30903b37c23"
},
{
"type":"match",
"id":"c1e56bff-db4c-470b-bb44-3c2068e33245"
},
{
"type":"match",
"id":"44d27efc-3cc6-4935-8bbe-702e4da44744"
},
{
"type":"match",
"id":"be3ebed3-9183-47f1-b4b0-40a2229dd5ff"
}
]
}
},
"links":{
"self":"https://api.pubg.com/shards/steam/players/account.82bad0072f31455d8d9f8d834da2f2f3",
"schema":""
}
}
],
"links":{
"self":"https://api.pubg.com/shards/steam/players?filter[playerNames]=Nomad_o7,TGLTN"
},
"meta":{
}
}
I don't know how it could work before since you have a bug in your class
[JsonProperty("matches")]
[JsonConverter(typeof(RelationshipIdConverter))]
public IEnumerable<string> MatchIds { get; set; }
but if you look at your json , you will see, that mathes is not a collection of strings, matches is a collection of objects
public partial class Relationships
{
[JsonProperty("assets")]
public Assets Assets { get; set; }
[JsonProperty("matches")]
public Matches MatchIds { get; set; }
}
public partial class Matches
{
[JsonProperty("data")]
public List<MatchesDatum> Data { get; set; }
}
public partial class MatchesDatum
{
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("id")]
public Guid Id { get; set; }
}
and IMHO you don't need any custom converter

Deserialize JSON with dynamic keys using C# Json.NET

From an external API I am receiving the below JSON response for the bank details of a customer.
{
"bankDetails":[
{
"ABC Bank":[
{
"sNo":1,
"acNo":"1235465",
"acBalance":"100.25"
},
{
"sNo":2,
"acNo":"1235467",
"acBalance":"50.25"
}
],
"bankName":"ABC Bank",
"totalAmount":"150.50"
},
{
"XYZ Bank":[
{
"sNo":1,
"acNo":"1248565",
"acBalance":"75.25"
}
],
"bankName":"XYZ Bank",
"totalAmount":"75.25"
},
{
"BCD Bank":[
{
"sNo":1,
"acNo":"145665",
"acBalance":"10.25"
},
{
"sNo":2,
"acNo":"195267",
"acBalance":"5.25"
}
],
"bankName":"BCD Bank",
"totalAmount":"15.50"
}
]
}
I need to deserialize this to a C# class using JSON.Net. What should be structure of the C# class as the first key is dynamic?. The first key with bank name returned will be different for each customer
The typical solution to dealing with dynamic keys is to use a Dictionary<string, T> in place of a regular class. See How can I deserialize a child object with dynamic (numeric) key names? for an example of this. However, that solution doesn't really work for your case, because there are other properties in the same object which do not have dynamic keys (the bankName and totalAmount), and the values of those properties are primitives whereas the value of dynamic property is an array of bank accounts. A better solution here is to use a JsonConverter.
Before we get to that, we need to set up a class structure to deserialize into. This is pretty straightforward:
class RootObject
{
public List<Bank> BankDetails { get; set; }
}
[JsonConverter(typeof(BankConverter))]
class Bank
{
public string BankName { get; set; }
public decimal TotalAmount { get; set; }
public List<Account> Accounts { get; set; }
}
class Account
{
[JsonProperty("sNo")]
public int SequenceNumber { get; set; }
[JsonProperty("acNo")]
public string AccountNumber { get; set; }
[JsonProperty("acBalance")]
public decimal Balance { get; set; }
}
You'll notice that I've added a few [JsonProperty] attributes in the Account class to map the shorthand property names in the JSON to friendlier property names in that class. And the [JsonConverter] attribute on the Bank class tells the serializer that we will be using a custom BankConverter to handle that class.
Here is the code for the BankConverter. It uses a JObject internally to make it easier to read and work with the JSON.
class BankConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Bank);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JObject obj = JObject.Load(reader);
Bank bank = new Bank();
// populate the known properties (bankName and totalAmount)
serializer.Populate(obj.CreateReader(), bank);
// now handle the dynamic key
bank.Accounts = obj[bank.BankName].ToObject<List<Account>>(serializer);
return bank;
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
With these classes in place, you can deserialize the JSON like this:
var root = JsonConvert.DeserializeObject<RootObject>(json);
Here is a working demo: https://dotnetfiddle.net/knsRLv

Deserializing JSON response which differ in each response

I have complicated JSON respone which containt about 100 attributes/objects/arrays and have difrrent responses in terms of object/array.
Firstly I have structure like that (when object exists)
{
'att1': 'desc',
'att2': '83482',
'att3': null,
'test': {
'object_array1': [
100
],
'object_array2': [
'desc'
]
}
}
public class Root
{
//fields here
public Test test { get; set; }
}
public class Test
{
public List<int> object_array1 { get; set; }
public List<string> object_array2 { get; set; }
}
The issue I have is when this objects is empty.
After that resposne is changing and returning empty array.
So it is chaning to this:
{
'att1': 'desc',
'att2': '83482',
'att3': null,
'test': [
]
}
And beacause of that I have standard error:
Additional information: Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'Test' because the type requires a JSON object (e.g. {"name":"value"}) to deserialize correctly.
I was trying to write customconverter with something like that:
private bool IsArray(string fieldName, JObject jObject)
{
return jObject[fieldName].Type == JTokenType.Array;
}
I'm using JSON.NET
But I failed miserably. Any help would be much appreciated
Use custom converter which would check token type that has been read in the ReadJson method and substitute some default value for test when the token is of type JArray (assuming it can only be an array when the test object is "empty"):
public class Root
{
[JsonConverter(typeof(TestIgnoreEmptyListConverter))]
public Test test { get; set; }
}
// .................
public class TestIgnoreEmptyListConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(Test).IsAssignableFrom(objectType) || objectType.IsArray;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var token = JToken.Load(reader);
if (token is JArray)
return default(Test);
else
return token.ToObject<Test>();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Demo: https://dotnetfiddle.net/Q3S5hX
Your JSON propert name does not match object's name. Serializer tries to serialize/deserialize using property name or attributes.
Change this to the right name
public Test test { get; set; }
Or use attribute to set json name for a property.
[JsonProperty("test")]
public Test objects { get; set; }
Or even better, refactor your code to respect C# conventions. (I suggest to rename fields to be more descriptive)
public class Root
{
[JsonProperty("test")]
public Test Test { get; set; }
}
public class Test
{
[JsonProperty("object_array1")]
public List<int> IntArray { get; set; }
[JsonProperty("object_array2")]
public List<string> StringArray { get; set; }
}

C# Deserialising Json data array with missing column names

I have the following Json Data:
{"status": "ok",
"data": [
{"2016-12-12 02:00:00": 29.6},
{"2016-12-12 02:30:00": 29.4},
{"2016-12-12 03:00:00": 28.9},
...... many more records
]}
In and ideal world, the data would include column/field names:
{"status": "ok",
"data": [
{"ts": "2016-12-12 02:00:00", "temp": 29.6},
{"ts": "2016-12-12 02:30:00", "temp": 29.4},
]}
However the column names are missing. I would like to know how to retrieve the data into the following Class Structure:
#region JSON Class
public class telemetryData
{
public string ts { get; set; }
public double temp { get; set; }
}
public class RootObject
{
public string status { get; set; }
public List<telemetryData> data { get; set; }
}
#endregion
I am deserialising the data using the following:
JavaScriptSerializer sr = new JavaScriptSerializer();
jsonResponse = sr.Deserialize<RootObject>(jsonString);
foreach (var item in jsonResponse.data)
{
OutputMoistureBuffer.AddRow();
OutputMoistureBuffer.ts = item.ts;
OutputMoistureBuffer.temp = item.temp;
}
This code is being used in an SQL Server 2008 R2 SSIS Package, using VS2008 C#. I am unable to 'step through' and debug the code in a Script component, and as such I am unable to get a accurate error message as to why I am unable to get it to work.
Any assistance on the correct formatting of my JSON Class would be greatly appreciated.
You can use Json.NET package for json deserialization. It allows to write custom converters
public class TelemetryDataConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(telemetryData);
}
public override object ReadJson(JsonReader reader,
Type objectType, object existingValue, JsonSerializer serializer)
{
var obj = JObject.Load(reader);
var property = (JProperty)obj.First;
return new telemetryData {
ts = property.Name,
temp = property.Value.Value<double>()
};
}
public override void WriteJson(JsonWriter writer,
object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Then you can either put custom converter attribute to your class, or pass converter to deserializer:
[JsonConverter(typeof(TelemetryDataConverter))]
public class telemetryData
{
public string ts { get; set; }
public double temp { get; set; }
}
Deserialization will look like:
var data = JsonConvert.DeserializeObject<RootObject>(json);
NOTE: Consider to parse date strings into DateTime objects. Also consider to use PascalCase naming for classes and properties.

JSON deserialization fails only when array is empty

On some occasions when I receive JSON whose one of the array property is empty the deserialization fails, throwing the following exception :
Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'SonicApi.ClickMark[]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly.
To fix this error either change the JSON to a JSON array (e.g. [1,2,3]) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object.
Path auftakt_result.click_marks, line 1, position 121.
Trying to ignore null values with the following code didn't help:
var jsonSerializerSettings = new JsonSerializerSettings();
jsonSerializerSettings.NullValueHandling = NullValueHandling.Ignore;
Here is an example of JSON that produces the error :
{
"status": {
"code": 200
},
"auftakt_result": {
"clicks_per_bar": 0,
"overall_tempo": 0,
"overall_tempo_straight": 0,
"click_marks": {}
}
}
Here is an example of JSON whose array is not empty and does not produce any error:
{
"status": {
"code": 200
},
"auftakt_result": {
"clicks_per_bar": 8,
"overall_tempo": 144.886978,
"overall_tempo_straight": 144.90889,
"click_marks": [
{
"index": 0,
"bpm": 144.226624,
"probability": 0.828170717,
"time": 0.0787981859,
"downbeat": "false"
},
{
"index": 1,
"bpm": 144.226517,
"probability": 0.831781149,
"time": 0.286802721,
"downbeat": "false"
},
etc ...
Here are the C# types representing above objects:
public sealed class AnalyzeTempoResponse
{
[JsonProperty("auftakt_result")]
public AuftaktResult AuftaktResult { get; set; }
[JsonProperty("status")]
public Status Status { get; set; }
}
public sealed class Status
{
[JsonProperty("code")]
public int Code { get; set; }
}
public sealed class AuftaktResult
{
[JsonProperty("clicks_per_bar")]
public int ClicksPerBar { get; set; }
[JsonProperty("overall_tempo")]
public double OverallTempo { get; set; }
[JsonProperty("overall_tempo_straight")]
public double OverallTempoStraight { get; set; }
[JsonProperty("click_marks")]
public ClickMark[] ClickMarks { get; set; }
}
public sealed class ClickMark
{
[JsonProperty("index")]
public int Index { get; set; }
[JsonProperty("bpm")]
public double Bpm { get; set; }
[JsonProperty("probability")]
public double Probability { get; set; }
[JsonProperty("time")]
public double Time { get; set; }
[JsonProperty("downbeat")]
public string Downbeat { get; set; }
}
How can I deserialize responses whose click_marks content is null ?
If that matters, I am using the latest version of Newtonsoft.Json : v6.0
EDIT
Here is the adopted solution according #khellang's answer :
public class ClickMarkArrayConverter : CustomCreationConverter<ClickMark[]>
{
public override ClickMark[] Create(Type objectType)
{
return new ClickMark[] {};
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.StartArray)
{
return serializer.Deserialize(reader, objectType);
}
if (reader.TokenType == JsonToken.StartObject)
{
serializer.Deserialize(reader); // NOTE : value must be consumed otherwise an exception will be thrown
return null;
}
throw new NotSupportedException("Should not occur, check JSON for a new type of malformed syntax");
}
}
It has nothing to do with null values (none of your JSON examples have null values for any property). You're trying to deserialize a JSON object into a ClickMark[]:
"click_marks": {} // <-- This is an object, not null, not an array.
The reason it works for example number two is that the click_marks property actually is an array of ClickMark objects:
"click_marks": [{...}, {...}, {...}] // <-- This is an array with three objects.
Where does the data come from? You need to make sure that the click_marks property is either an array or an object, not both, and that your typed C# object ClickMarks matches the "type" of the JSON property.
If you have no control over the data, e.g. if it comes from a 3rd party, I'd suggest you write a custom JsonConverter that you can apply to that single property:
public class ObjectToArrayConverter<T> : CustomCreationConverter<T[]>
{
public override T[] Create(Type objectType)
{
return new T[0];
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.StartArray)
{
return serializer.Deserialize(reader, objectType);
}
else
{
return new T[] { serializer.Deserialize<T>(reader) };
}
}
}
And apply it like this:
public sealed class AuftaktResult
{
// ...
[JsonProperty("click_marks")]
[JsonConverter(typeof(ObjectToArrayConverter<ClickMark>))]
public ClickMark[] ClickMarks { get; set; }
}
This will check if the value is a single object and wrap it in an array so it will match your C# POCO property :)

Categories