I want to deserialise JSON from a web response. Here's a typical response:
{
"response": {
"garbage": 0,
"details": [
{
"id": "123456789"
}
]
}
}
However, this format is undesirable. Ideally, the response would be just
{
"id": "123456789"
}
so that it could be deserialised into an object like
public class Details {
[JsonProperty("id")]
public ulong Id { get; set; }
}
Since I have no control of the server (it's a public API), I aim to modify the deserialisation process to achieve my desired format.
I've tried to make use of a custom JsonConverter
to accomplish this. The idea is to skip tokens until I find the desired starting point for deserialisation into Details
. However, I'm not sure where it should be used in the deserialisation process.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Newtonsoft.Json;
namespace ConsoleApp2 {
class Program {
static void Main(string[] args) {
// Simulating a stream from WebResponse.
const string json = "{"response":{"garbage":0,"details":[{"id":"123456789"}]}}";
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(json);
Stream stream = new MemoryStream(bytes);
Details details = Deserialise<Details>(stream);
Console.WriteLine($"ID: {details.Id}");
Console.Read();
}
public static T Deserialise<T>(Stream stream) where T : class {
using (StreamReader reader = new StreamReader(stream, Encoding.UTF8)) {
JsonSerializerSettings settings = new JsonSerializerSettings {
MissingMemberHandling = MissingMemberHandling.Ignore,
DateParseHandling = DateParseHandling.None
};
settings.Converters.Add(new DetailConverter());
return JsonSerializer.Create(settings).Deserialize(reader, typeof(T)) as T;
}
}
}
public class Details {
[JsonProperty("id")]
public ulong Id { get; set; }
}
public class DetailConverter : 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) {
if (reader.Depth == 0) {
while (!(reader.Path.Equals("response.details[0]", StringComparison.OrdinalIgnoreCase) && reader.TokenType == JsonToken.StartObject)) {
reader.Read();
}
try {
return serializer.Deserialize(reader, objectType);
} finally {
reader.Read(); // EndArray - details
reader.Read(); // EndObject - response
reader.Read(); // EndObject - root
}
}
return serializer.Deserialize(reader, objectType);
}
public override bool CanWrite => false;
public override bool CanConvert(Type objectType) => true;
}
}
As things stand right now, the stack overflows because DetailConverter.ReadJson()
is being used on the same object repeatedly and it never gets deserialised. I think it's because I've set DetailConverter
as a "global" converter through JsonSerializerSettings
. I think the issues lies in when and how my converter is being used rather than in its inner workings.
I've gotten a similar DetailConverter
to work for the following structure. However, while the array from details
is removed, it's still undesirable because of nesting and unused properties.
public class Root {
[JsonProperty("response")]
public Response Response { get; set; }
}
public class Response {
[JsonProperty("details")]
[JsonConverter(typeof(DetailConverter))]
public Details Details { get; set; }
[JsonProperty("garbage")]
public uint Garbage { get; set; }
}
public class Details {
[JsonProperty("id")]
public ulong Id { get; set; }
}
I thought it'd be straightforward to scale up the converter to the entire JSON rather than just one property. Where did I go wrong?
The solution is to clear the list of converters of the JsonSerializer
before calling JsonSerializer.Deserialize
from my JsonConverter
.
I got the idea from here, in which a user describes how nulling out the JsonConverter
of a JsonContract
reverts to the default JsonConverter
. This avoids the problem of repeatedly calling my custom JsonConverter
for the child object I wish to deserialise.
public override object ReadJson(JsonReader reader,
Type objectType,
object existingValue,
JsonSerializer serializer) {
while (reader.Depth != 3) {
reader.Read();
}
serializer.Converters.Clear();
return serializer.Deserialize(reader, objectType);
}
The code has also been simplified, removing the try
- finally
block. It's not necessary to read the ending tokens because there will never be an attempt to deserialise them anyway. There is also no need to check for the depth because the custom JsonConverter
will only be used once.