How to serialize Task<TResult> with Json.NET? - c#

Consider
public class InQuestion<TType>
{
// JsonConverter(typeof CustomConverter))
public Task<TType> toConvert { get; set; }
}
How can I (de)serialize this class with json.net?
What I think I actually want to serialize is the underlying Task< T >.Result, which can then be deserialized with Task< T >.FromResult(). If I am to use custom JsonConverter, I cannot pass generic TType through Attribute, to reconstruct (or retrieve) TType object in the JsonConverter. Hence I'm stuck.
Question came to be from this code:
public class Program
{
public class InQuestion<TType>
{
public Task<TType> toConvert { get; set; }
}
public class Result
{
public int value { get; set; }
}
public static async Task Main()
{
var questionable = new InQuestion<Result>();
questionable.toConvert = Task.Run(async () => new Result { value = 42 });
await questionable.toConvert;
string json = JsonConvert.SerializeObject(questionable);
Debug.WriteLine(json);
InQuestion<Result> back = JsonConvert.DeserializeObject(json, typeof(InQuestion<Result>)) as InQuestion<Result>;
Debug.Assert(back?.toConvert?.Result?.value == 42);
}
}
which, surprisingly to me, halts, during the call to JsonConvert.DeserializeObject. https://github.com/JamesNK/Newtonsoft.Json/issues/1886 talks about the issue and recommends reasonable "Don't ever serialize/deserialize task.", but doesn't actually advice how to serialize the underlying Task< T >.Result.

A Task is a promise of a future value, and of course you cannot serialize a value that has yet to be provided.
Because the InQuestion object holds a Task member, you cannot serialize and deserialize the InQuestion object.
The workaround is to serialize the result, and reconstruct the InQuestion object after deserialization.
public static async Task Main()
{
var questionable = new InQuestion<Result>();
questionable.toConvert = Task.Run(async () => new Result { value = 42 });
Result result = await questionable.toConvert;
string json = JsonConvert.SerializeObject(result);
Result back = JsonConvert.DeserializeObject(json, typeof<Result>) as Result;
InQuestion<Result> reconstructed = new InQuestion<Result>()
{
toConvert = Task.FromResult(back)
};
}

I have found two solutions to this problem.
From the Add support for generic JsonConverter instantiation:
[JsonConverter(typeof(InQuestionConverter<>))]
public class InQuestion<TResult>
{
public Task<TResult> toConvert { get; set; }
}
public class Result
{
public int value { get; set; }
public string text { get; set; }
public override bool Equals(object obj)
{
return obj is Result result &&
value == result.value &&
text == result.text;
}
}
public class InQuestionConverter<TResult> : JsonConverter<InQuestion<TResult>>
{
public override InQuestion<TResult> ReadJson(JsonReader reader, Type objectType, InQuestion<TResult> existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (hasExistingValue)
existingValue.toConvert = Task.FromResult(serializer.Deserialize<TResult>(reader));
else
existingValue = new InQuestion<TResult>
{
toConvert = Task.FromResult(serializer.Deserialize<TResult>(reader))
};
return existingValue;
}
public override void WriteJson(JsonWriter writer, InQuestion<TResult> value, JsonSerializer serializer)
{
serializer.Serialize(writer, value.toConvert.Result, typeof(TResult));
}
}
public sealed class CustomContractResolver : DefaultContractResolver
{
protected override JsonConverter ResolveContractConverter(Type objectType)
{
var typeInfo = objectType.GetTypeInfo();
if (typeInfo.IsGenericType && !typeInfo.IsGenericTypeDefinition)
{
var jsonConverterAttribute = typeInfo.GetCustomAttribute<JsonConverterAttribute>();
if (jsonConverterAttribute != null && jsonConverterAttribute.ConverterType.GetTypeInfo().IsGenericTypeDefinition)
{
Type t = jsonConverterAttribute.ConverterType.MakeGenericType(typeInfo.GenericTypeArguments);
object[] parameters = jsonConverterAttribute.ConverterParameters;
return (JsonConverter)Activator.CreateInstance(t, parameters);
}
}
return base.ResolveContractConverter(objectType);
}
}
public static void Main()
{
var questionable = new InQuestion<Result>();
questionable.toConvert = Task.Run(async () => { return new Result { value = 42, text = "fox" }; });
questionable.toConvert.Wait();
string json = JsonConvert.SerializeObject(questionable, Formatting.None, new JsonSerializerSettings { ContractResolver = new CustomContractResolver() });
InQuestion<Result> back = JsonConvert.DeserializeObject(json, typeof(InQuestion<Result>), new JsonSerializerSettings { ContractResolver = new CustomContractResolver() }) as InQuestion<Result>;
Debug.Assert(back.toConvert.Result.Equals(questionable.toConvert.Result));
return;
}
Enables a custom ContractResolver which will point to correct generic instantiation of JsonConverter<TResult>, in which serialization is straightforward. This requires configuring JsonSerializerSettings and providing serialization for the entire InQuestion class (note that converter doesn't check for Task.IsCompleted in this sample).
Alternatively, using JsonConverterAttribute just on properties of type Task<T> and relying on reflection to retrieve TResult type from non-generic Converter:
public class InQuestion<TResult>
{
[JsonConverter(typeof(FromTaskOfTConverter))]
public Task<TResult> toConvert { get; set; }
}
public class FromTaskOfTConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return IsDerivedFromTaskOfT(objectType);
}
static bool IsDerivedFromTaskOfT(Type type)
{
while (type.BaseType != typeof(object))
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>))
return true;
type = type.BaseType;
}
return false;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
Debug.Assert(IsDerivedFromTaskOfT(objectType));
Type TResult = objectType.GetGenericArguments()[0];
object ResultValue = serializer.Deserialize(reader, TResult);
return typeof(Task).GetMethod("FromResult").MakeGenericMethod(TResult).Invoke(null, new[] { ResultValue });
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
Type objectType = value.GetType();
Debug.Assert(IsDerivedFromTaskOfT(objectType));
Type TResult = objectType.GetGenericArguments()[0];
Type TaskOfTResult = typeof(Task<>).MakeGenericType(TResult);
if ((bool)TaskOfTResult.GetProperty("IsCompleted").GetValue(value) == true)
{
object ResultValue = TaskOfTResult.GetProperty("Result").GetValue(value);
serializer.Serialize(writer, ResultValue, TResult);
}
else
{
serializer.Serialize(writer, Activator.CreateInstance(TResult));
}
}
}
public static void Main()
{
var questionable = new InQuestion<Result>();
questionable.toConvert = Task.Run(async () => { return new Result { value = 42, text = "fox" }; });
questionable.toConvert.Wait();
string json = JsonConvert.SerializeObject(questionable);
InQuestion<Result> back = JsonConvert.DeserializeObject(json, typeof(InQuestion<Result>)) as InQuestion<Result>;
Debug.Assert(back.toConvert.Result.Equals(questionable.toConvert.Result));
return;
}
With all that, I won't mark this accepted, since I lack understanding in both generics reflection and json.net.

Related

C# Converting Json List to List objects using JsonConverter

I am attempting to convert lists in Json (using Newtonsoft.json), to a custom "Element" structure, where I will have many classes all with a property named "Element" in it, but some arbitrary type.
An example used in the code below is this json:
{
"List_A": [1, 2],
"List_B": ["Hello", "World"]
}
In the first property "List_A", the numbers need to be converted into AΞElement class, that has a property Element of type int.
In the second property "List_B", these strings need to be converted into a different class BΞElement.
Below is working code where I am trying to fill in the ReadJson function of the ListConverter class I have created, that is a JsonConverter. I am trying to figure out how to read stuff out of the serializer, and create my own custom objects.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace ConsoleApp1
{
class Program
{
[DataContract]
public class AΞElement
{
[DataMember]
int _Element;
public int Element
{
get => _Element;
set => _Element = value;
}
}
[DataContract]
public class BΞElement
{
[DataMember]
string _Name;
public string Element
{
get => _Name;
set => _Name = value;
}
}
[DataContract]
public class C
{
[DataMember]
List<AΞElement> List_A { get; set; } = new List<AΞElement>();
[DataMember]
List<BΞElement> List_B { get; set; } = new List<BΞElement>();
}
static void Main(string[] args)
{
var json = "{ \"List_A\": [1, 2], \"List_B\": [\"Hello\", \"World\"] }";
var c = JsonConvert.DeserializeObject<C>(json, new JsonSerializerSettings()
{
Converters = new List<JsonConverter> { new ListConverter() }
});
}
public class ListConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(IList).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (existingValue != null)
{
var list = (IList)existingValue;
list.Clear();
}
var elementType = existingValue.GetType().GetGenericArguments()[0];
if (elementType.Name.EndsWith("ΞElement"))
{
/// What do I put here??
}
else
{
serializer.Populate(reader, existingValue);
}
return existingValue;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
}
}
Note: It is imperative that I keep the class structures of ΞElement and the json the same.
If I understood correctly, I've come to a solution like this:
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (existingValue != null)
{
var list = (IList)existingValue;
list.Clear();
}
var elementType = existingValue.GetType().GetGenericArguments()[0];
if (elementType.Name.EndsWith("ΞElement"))
{
var list = (IList)existingValue;
if (elementType.Name.EndsWith("AΞElement"))
{
while (reader.Read() && reader.Value != null)
{
list.Add(new AΞElement
{
Element = Convert.ToInt32(reader.Value)
});
}
}
else if (elementType.Name.EndsWith("BΞElement"))
{
while (reader.Read() && reader.Value != null)
{
list.Add(new BΞElement
{
Element = (string)reader.Value
});
}
}
}
else
{
serializer.Populate(reader, existingValue);
}
return existingValue;
}
Basically I'm checking for type of AΞElement and BΞElement, reading from JsonReader and adding the values in array one by one into the relevant list until no more tokens to read.

multiple JsonProperty Name assigned to single property

I have two format of JSON which I want to Deserialize to one class.
I know we can't apply two [JsonProperty] attribute to one property.
Can you please suggest me a way to achieve this?
string json1 = #"
{
'field1': '123456789012345',
'specifications': {
'name1': 'HFE'
}
}";
string json2 = #"
{
'field1': '123456789012345',
'specifications': {
'name2': 'HFE'
}
}";
public class Specifications
{
[JsonProperty("name1")]
public string CodeModel { get; set; }
}
public class ClassToDeserialize
{
[JsonProperty("field1")]
public string Vin { get; set; }
[JsonProperty("specification")]
public Specifications Specifications { get; set; }
}
I want name1 and name2 both to be deserialize to name1 property of specification class.
A simple solution which does not require a converter: just add a second, private property to your class, mark it with [JsonProperty("name2")], and have it set the first property:
public class Specifications
{
[JsonProperty("name1")]
public string CodeModel { get; set; }
[JsonProperty("name2")]
private string CodeModel2 { set { CodeModel = value; } }
}
Fiddle: https://dotnetfiddle.net/z3KJj5
Tricking custom JsonConverter worked for me.
Thanks #khaled4vokalz, #Khanh TO
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
object instance = objectType.GetConstructor(Type.EmptyTypes).Invoke(null);
PropertyInfo[] props = objectType.GetProperties();
JObject jo = JObject.Load(reader);
foreach (JProperty jp in jo.Properties())
{
if (string.Equals(jp.Name, "name1", StringComparison.OrdinalIgnoreCase) || string.Equals(jp.Name, "name2", StringComparison.OrdinalIgnoreCase))
{
PropertyInfo prop = props.FirstOrDefault(pi =>
pi.CanWrite && string.Equals(pi.Name, "CodeModel", StringComparison.OrdinalIgnoreCase));
if (prop != null)
prop.SetValue(instance, jp.Value.ToObject(prop.PropertyType, serializer));
}
}
return instance;
}
I had the same use case, though in Java.
Resource that helped https://www.baeldung.com/json-multiple-fields-single-java-field
We can use a
#JsonProperty("main_label_to_serialize_and_deserialize")
#JsonAlias("Alternate_label_if_found_in_json_will_be_deserialized")
In your use case you could do
#JsonProperty("name1")
#JsonAlias("name2")
You can do it using a JsonConverter.
It's useful for example when you consume some data from 3rd party services and they keep changing property names and then going back to previous property names. :D
The following code shows how to deserialize from multiple property names to the same class property decorated with a [JsonProperty(PropertyName = "EnrollmentStatusEffectiveDateStr")] attribute.
The class MediCalFFSPhysician is also decorated with the custom JsonConverter: [JsonConverter(typeof(MediCalFFSPhysicianConverter))]
Note that _propertyMappings dictionary holds the possible property names that should be mapped to the property EnrollmentStatusEffectiveDateStr:
private readonly Dictionary<string, string> _propertyMappings = new()
{
{"Enrollment_Status_Effective_Dat", "EnrollmentStatusEffectiveDateStr"},
{"Enrollment_Status_Effective_Date", "EnrollmentStatusEffectiveDateStr"},
{"USER_Enrollment_Status_Effectiv", "EnrollmentStatusEffectiveDateStr"}
};
Full code:
// See https://aka.ms/new-console-template for more information
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using System.Reflection;
using System.Text.Json;
internal class JSONDeserialization
{
private static void Main(string[] args)
{
var jsonPayload1 = $"{{\"Enrollment_Status_Effective_Dat\":\"2022/10/13 19:00:00+00\"}}";
var jsonPayload2 = $"{{\"Enrollment_Status_Effective_Date\":\"2022-10-13 20:00:00+00\"}}";
var jsonPayload3 = $"{{\"USER_Enrollment_Status_Effectiv\":\"2022-10-13 21:00:00+00\"}}";
var deserialized1 = JsonConvert.DeserializeObject<MediCalFFSPhysician>(jsonPayload1);
var deserialized2 = JsonConvert.DeserializeObject<MediCalFFSPhysician>(jsonPayload2);
var deserialized3 = JsonConvert.DeserializeObject<MediCalFFSPhysician>(jsonPayload3);
Console.WriteLine(deserialized1.Dump());
Console.WriteLine(deserialized2.Dump());
Console.WriteLine(deserialized3.Dump());
Console.ReadKey();
}
}
public class MediCalFFSPhysicianConverter : JsonConverter
{
private readonly Dictionary<string, string> _propertyMappings = new()
{
{"Enrollment_Status_Effective_Dat", "EnrollmentStatusEffectiveDateStr"},
{"Enrollment_Status_Effective_Date", "EnrollmentStatusEffectiveDateStr"},
{"USER_Enrollment_Status_Effectiv", "EnrollmentStatusEffectiveDateStr"}
};
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override bool CanConvert(Type objectType)
{
return objectType.GetTypeInfo().IsClass;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
{
object instance = Activator.CreateInstance(objectType);
var props = objectType.GetTypeInfo().DeclaredProperties.ToList();
JObject jo = JObject.Load(reader);
foreach (JProperty jp in jo.Properties())
{
if (!_propertyMappings.TryGetValue(jp.Name, out var name))
name = jp.Name;
PropertyInfo prop = props.FirstOrDefault(pi =>
pi.CanWrite && pi.GetCustomAttribute<JsonPropertyAttribute>().PropertyName == name);
prop?.SetValue(instance, jp.Value.ToObject(prop.PropertyType, serializer));
}
return instance;
}
}
[JsonConverter(typeof(MediCalFFSPhysicianConverter))]
public class MediCalFFSPhysician
{
[JsonProperty(PropertyName = "EnrollmentStatusEffectiveDateStr")]
public string EnrollmentStatusEffectiveDateStr { get; set; }
}
public static class ObjectExtensions
{
public static string Dump(this object obj)
{
try
{
return System.Text.Json.JsonSerializer.Serialize(obj, new JsonSerializerOptions { WriteIndented = true });
}
catch (Exception)
{
return string.Empty;
}
}
}
The output is this:
Adapted from: Deserializing different JSON structures to the same C# class
You could even do this for more than 2 names.
#JsonProperty("name1")
#JsonAlias({"name2","name3","name4"})

Using custom JsonConverter and TypeNameHandling in Json.net

I have a class with an interface-typed property like:
public class Foo
{
public IBar Bar { get; set; }
}
I also have multiple concrete implementations of the IBar interface that can be set at runtime. Some of these concrete classes require a custom JsonConverter for serialization & deserialization.
Utilizing the TypeNameHandling.Auto option the non-convertor requiring IBar classes can be serialized and deserialized perfectly. The custom-serialized classes on the other hand have no $type name output and while they are serialized as expected, they cannot be deserialized to their concrete type.
I attempted to write-out the $type name metadata myself within the custom JsonConverter; however, on deserialization the converter is then being bypassed entirely.
Is there a workaround or proper way of handling such a situation?
I solved the similar problem and I found a solution. It's not very elegant and I think there should be a better way, but at least it works. So my idea was to have JsonConverter per each type that implements IBar and one converter for IBar itself.
So let's start from models:
public interface IBar { }
public class BarA : IBar { }
public class Foo
{
public IBar Bar { get; set; }
}
Now let's create converter for IBar. It will be used only when deserializing JSON. It will try to read $type variable and call converter for implementing type:
public class BarConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotSupportedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jObj = JObject.Load(reader);
var type = jObj.Value<string>("$type");
if (type == GetTypeString<BarA>())
{
return new BarAJsonConverter().ReadJson(reader, objectType, jObj, serializer);
}
// Other implementations if IBar
throw new NotSupportedException();
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof (IBar);
}
public override bool CanWrite
{
get { return false; }
}
private string GetTypeString<T>()
{
var typeOfT = typeof (T);
return string.Format("{0}, {1}", typeOfT.FullName, typeOfT.Assembly.GetName().Name);
}
}
And this is converter for BarA class:
public class BarAJsonConverter : BarBaseJsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// '$type' property will be added because used serializer has TypeNameHandling = TypeNameHandling.Objects
GetSerializer().Serialize(writer, value);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var existingJObj = existingValue as JObject;
if (existingJObj != null)
{
return existingJObj.ToObject<BarA>(GetSerializer());
}
throw new NotImplementedException();
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(BarA);
}
}
You may notice that it's inherited from BarBaseJsonConverter class, not JsonConverter. And also we do not use serializer parameter in WriteJson and ReadJson methods. There is a problem with using serializer parameter inside custom converters. You can read more here. We need to create new instance of JsonSerializer and base class is a good candidate for that:
public abstract class BarBaseJsonConverter : JsonConverter
{
public JsonSerializer GetSerializer()
{
var serializerSettings = JsonHelper.DefaultSerializerSettings;
serializerSettings.TypeNameHandling = TypeNameHandling.Objects;
var converters = serializerSettings.Converters != null
? serializerSettings.Converters.ToList()
: new List<JsonConverter>();
var thisConverter = converters.FirstOrDefault(x => x.GetType() == GetType());
if (thisConverter != null)
{
converters.Remove(thisConverter);
}
serializerSettings.Converters = converters;
return JsonSerializer.Create(serializerSettings);
}
}
JsonHelper is just a class to create JsonSerializerSettings:
public static class JsonHelper
{
public static JsonSerializerSettings DefaultSerializerSettings
{
get
{
return new JsonSerializerSettings
{
Converters = new JsonConverter[] { new BarConverter(), new BarAJsonConverter() }
};
}
}
}
Now it will work and you still can use your custom converters for both serialization and deserialization:
var obj = new Foo { Bar = new BarA() };
var json = JsonConvert.SerializeObject(obj, JsonHelper.DefaultSerializerSettings);
var dObj = JsonConvert.DeserializeObject<Foo>(json, JsonHelper.DefaultSerializerSettings);
Using information from Alesandr Ivanov's answer above, I created a generic WrappedJsonConverter<T> class that wraps (and unwraps) concrete classes requiring a converter using a $wrappedType metadata property that follows the same type name serialization as the standard $type.
The WrappedJsonConverter<T> is added as a converter to the Interface (ie. IBar), but otherwise this wrapper is completely transparent to classes that do not require a converter and also requires no changes to the wrapped converters.
I used a slightly different hack to get around the converter/serializer looping (static fields), but it does not require any knowledge of the serializer settings being used, and allows for the IBar object graph to have child IBar properties.
For wrapped objects the Json looks like:
"IBarProperty" : {
"$wrappedType" : "Namespace.ConcreteBar, Namespace",
"$wrappedValue" : {
"ConvertedID" : 90,
"ConvertedPropID" : 70
...
}
}
The full gist can be found here.
public class WrappedJsonConverter<T> : JsonConverter<T> where T : class
{
[ThreadStatic]
private static bool _canWrite = true;
[ThreadStatic]
private static bool _canRead = true;
public override bool CanWrite
{
get
{
if (_canWrite)
return true;
_canWrite = true;
return false;
}
}
public override bool CanRead
{
get
{
if (_canRead)
return true;
_canRead = true;
return false;
}
}
public override T ReadJson(JsonReader reader, T existingValue, JsonSerializer serializer)
{
var jsonObject = JObject.Load(reader);
JToken token;
T value;
if (!jsonObject.TryGetValue("$wrappedType", out token))
{
//The static _canRead is a terrible hack to get around the serialization loop...
_canRead = false;
value = jsonObject.ToObject<T>(serializer);
_canRead = true;
return value;
}
var typeName = jsonObject.GetValue("$wrappedType").Value<string>();
var type = JsonExtensions.GetTypeFromJsonTypeName(typeName, serializer.Binder);
var converter = serializer.Converters.FirstOrDefault(c => c.CanConvert(type) && c.CanRead);
var wrappedObjectReader = jsonObject.GetValue("$wrappedValue").CreateReader();
wrappedObjectReader.Read();
if (converter == null)
{
_canRead = false;
value = (T)serializer.Deserialize(wrappedObjectReader, type);
_canRead = true;
}
else
{
value = (T)converter.ReadJson(wrappedObjectReader, type, existingValue, serializer);
}
return value;
}
public override void WriteJson(JsonWriter writer, T value, JsonSerializer serializer)
{
var type = value.GetType();
var converter = serializer.Converters.FirstOrDefault(c => c.CanConvert(type) && c.CanWrite);
if (converter == null)
{
//This is a terrible hack to get around the serialization loop...
_canWrite = false;
serializer.Serialize(writer, value, type);
_canWrite = true;
return;
}
writer.WriteStartObject();
{
writer.WritePropertyName("$wrappedType");
writer.WriteValue(type.GetJsonSimpleTypeName());
writer.WritePropertyName("$wrappedValue");
converter.WriteJson(writer, value, serializer);
}
writer.WriteEndObject();
}
}

Json.NET deserialization of Tuple<...> inside another type doesn't work?

Using Json.net, deserializing a type that contains a Tuple<...> doesn't work (serialization works, but deserialization doesn't):
[TestMethod]
public void Test()
{
var orig = new TupleHolder("what????", true);
var json = JsonConvert.SerializeObject(orig);
Assert.AreEqual("{\"Tup\":{\"Item1\":\"what????\",\"Item2\":true}}", json);
// great! serialization works like a charm! now let's test deserialization:
var dupl = JsonConvert.DeserializeObject<TupleHolder>(json);
Assert.AreEqual("ZZZ", dupl.Tup.Item1); // pass! but it should be "what????"... what????
Assert.AreEqual(false, dupl.Tup.Item2); // pass! but it should be "true", right???
Assert.AreEqual(orig.Tup.Item1, dupl.Tup.Item1); // fail!
Assert.AreEqual(orig.Tup.Item2, dupl.Tup.Item2); // fail!
}
public class TupleHolder
{
public Tuple<string, bool> Tup { get; set; }
public TupleHolder() { Tup = new Tuple<string, bool>("ZZZ", false); }
public TupleHolder(string s, bool b) { Tup = new Tuple<string, bool>(s, b); }
}
Funny thing is that direct deserialization of Tuple<...> does work:
[TestMethod]
public void Test2()
{
var orig = new Tuple<string, bool>("ABC", true);
var json = JsonConvert.SerializeObject(orig);
var dupl = JsonConvert.DeserializeObject<Tuple<string, bool>>(json);
Assert.AreEqual(orig, dupl); // direct deserialization of Tuple<...> works.
}
Is it a Json.NET bug or am I missing here something?
The answer provided by Remi helped me. I took his TupleConverter and made it generic for a 2-tuple. The concept is the same for any N-tuple.
I leave it here in case it helps someone.
public class TupleConverter<U, V> : Newtonsoft.Json.JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(Tuple<U, V>) == objectType;
}
public override object ReadJson(
Newtonsoft.Json.JsonReader reader,
Type objectType,
object existingValue,
Newtonsoft.Json.JsonSerializer serializer)
{
if (reader.TokenType == Newtonsoft.Json.JsonToken.Null)
return null;
var jObject = Newtonsoft.Json.Linq.JObject.Load(reader);
var target = new Tuple<U, V>(
jObject["m_Item1"].ToObject<U>(), jObject["m_Item2"].ToObject<V>());
return target;
}
public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
}
Note: My Tuple was JSON serialized with m_Item1 and m_Item2, so I had to change jObject["ItemX"] to jObject["m_ItemX"]
Usage example with a List<Tuple<int, User>>:
string result = "String to deserialize";
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.Converters.Add(new TupleConverter<int, User>());
List<Tuple<int, User>> users = JsonConvert.DeserializeObject<List<Tuple<int, User>>>(result, settings);
The solution - or mine, anyhow - is to define a custom converter for the Tuple.
This example provides a concrete solution for a specific Tuple, but you could genericize it to make the TupleConverter class to handle any combination of value types. Could also make it abstract and have derived types implement instantiation methods for each item, to handle tuples with reference types.
public class TupleConverter : Newtonsoft.Json.JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(Tuple<string, bool>) == objectType;
}
public override object ReadJson(
Newtonsoft.Json.JsonReader reader,
Type objectType,
object existingValue,
Newtonsoft.Json.JsonSerializer serializer)
{
if (reader.TokenType == Newtonsoft.Json.JsonToken.Null)
return null;
var jObject = Newtonsoft.Json.Linq.JObject.Load(reader);
var target = new Tuple<string, bool>(
(string)jObject["Item1"], (bool)jObject["Item2"]);
return target;
}
public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
}
public class TupleHolder
{
[Newtonsoft.Json.JsonConverter(typeof(TupleConverter))]
public Tuple<string, bool> Tup { get; set; }
public TupleHolder() { Tup = new Tuple<string, bool>("ZZZ", false); }
public TupleHolder(string s, bool b) { Tup = new Tuple<string, bool>(s, b); }
}
[Test]
public void Test()
{
var orig = new TupleHolder("what????", true);
var json = Newtonsoft.Json.JsonConvert.SerializeObject(orig);
Assert.AreEqual("{\"Tup\":{\"Item1\":\"what????\",\"Item2\":true}}", json);
var dupl = Newtonsoft.Json.JsonConvert.DeserializeObject<TupleHolder>(json);
// These succeed, now
Assert.AreEqual(orig.Tup.Item1, dupl.Tup.Item1);
Assert.AreEqual(orig.Tup.Item2, dupl.Tup.Item2);
}
I ended up with something more generic, hope it helps
public class TupleConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
var match = Regex.Match(objectType.Name, "Tuple`([0-9])", RegexOptions.IgnoreCase);
return match.Success;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
try
{
var tupleTypes = objectType.GetProperties().ToList().Select(p => p.PropertyType).ToArray();
var jObject = Newtonsoft.Json.Linq.JObject.Load(reader);
var valueItems = new List<object>();
for (var i = 1; i <= tupleTypes.Length; i++)
valueItems.Add(jObject[$"m_Item{i}"].ToObject(tupleTypes[i - 1]));
var convertedObject = objectType.GetConstructor(tupleTypes)?.Invoke(valueItems.ToArray());
return convertedObject;
}
catch (Exception ex)
{
throw new Exception("Something went wrong in this implementation", ex);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
}

Dealing with JSON field that holds different types in C# [duplicate]

This question already has answers here:
How to handle both a single item and an array for the same property using JSON.net
(9 answers)
Closed 8 years ago.
I have to read a JSON document, which has a field that can contain different types.
For example can be either a long or an array of integers. I know I will need to use a custom deserializer, but am not sure how.
In the example below the xx field sometimes is a long, otherwise an array of ints.
Any help on how to deal with this is appreciated.
static void JsonTest() {
const string json = #"
{
'Code': 'XYZ',
'Response': {
'Type' : 'S',
'Docs': [
{
'id' : 'test1',
'xx' : 1
},
{
'id' : 'test2',
'xx' : [1, 2, 4, 8]
},
]
}
}";
A a;
try {
a = JsonConvert.DeserializeObject<A>(json);
}
catch( Exception ex ) {
Console.Error.WriteLine(ex.Message);
}
}
public class A {
public string Code;
public TResponse Response;
}
public class TResponse {
public string Type;
public List<Doc> Docs;
}
public class Doc {
public string id;
public int[] xx;
}
My implementation based on the suggestion below (changed array to long from int):
[JsonConverter(typeof(DocConverter))]
public class Doc {
public string id;
public long[] xx;
}
public class DocConverter : JsonConverter {
public override bool CanWrite { get { return false; } }
public override bool CanConvert( Type objectType ) {
return typeof(Doc).IsAssignableFrom(objectType);
}
public override object ReadJson( JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer ) {
JObject item = JObject.Load(reader);
Doc doc = new Doc();
doc.id = item["id"].ToObject<string>();
if( item["xx"].Type == JTokenType.Long )
doc.xx = new [] { item["xx"].ToObject<long>() };
else
doc.xx = item["xx"].ToObject<long[]>();
return doc;
}
public override void WriteJson( JsonWriter writer, object value, JsonSerializer serializer ) {
throw new NotImplementedException();
}
}
Since xx can either be a long or an array of ints, it makes sense to turn Doc into a class hierarchy. (If it were a single long or an array of longs, it would make sense to read them all into a single class.)
You can do this by using a JsonConverter, like so:
[JsonConverter(typeof(DocConverter))]
public abstract class Doc
{
public string id;
}
[JsonConverter(typeof(NoConverter))] // Prevents infinite recursion when converting a class instance known to be of type DocSingle
public class DocSingle : Doc
{
public long xx;
}
[JsonConverter(typeof(NoConverter))] // Prevents infinite recursion when converting a class instance known to be of type DocList
public class DocList : Doc
{
public int[] xx;
}
public class DocConverter : JsonConverter
{
public override bool CanWrite { get { return false; } }
public override bool CanConvert(Type objectType)
{
return typeof(Doc).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader,
Type objectType, object existingValue, JsonSerializer serializer)
{
JObject item = JObject.Load(reader);
if (item["xx"].Type == JTokenType.Integer)
{
return item.ToObject<DocSingle>();
}
else
{
return item.ToObject<DocList>();
}
}
public override void WriteJson(JsonWriter writer,
object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
public class NoConverter : JsonConverter
{
public override bool CanRead { get { return false; } }
public override bool CanWrite { get { return false; } }
public override bool CanConvert(Type objectType)
{
return false;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Update
incidentally, if you're willing to simplify your data model to say that xx can either be a single long or an array of longs, you can simplify the code as follows:
[JsonConverter(typeof(DocConverter))]
public sealed class Doc
{
public string id;
public long[] xx;
}
public class DocConverter : JsonConverter
{
public override bool CanWrite { get { return true; } }
public override bool CanConvert(Type objectType)
{
return typeof(Doc).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader,
Type objectType, object existingValue, JsonSerializer serializer)
{
JObject item = JObject.Load(reader);
var doc = new Doc();
JToken id = item["id"];
if (id != null)
doc.id = id.ToString();
JToken xx = item["xx"];
if (xx != null)
{
if (xx.Type == JTokenType.Integer)
{
var val = (long)xx;
doc.xx = new long[] { val };
}
else if (xx.Type == JTokenType.Array)
{
var val = xx.ToObject<long[]>();
doc.xx = val;
}
else
{
Debug.WriteLine("Unknown type of JToken for \"xx\": " + xx.ToString());
}
}
return doc;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var doc = (Doc)value;
writer.WriteStartObject();
writer.WritePropertyName("id");
writer.WriteValue(doc.id);
var xx = doc.xx;
if (xx != null)
{
writer.WritePropertyName("xx");
if (xx.Length == 1)
{
writer.WriteValue(xx[0]);
}
else
{
writer.WriteStartArray();
foreach (var x in xx)
{
writer.WriteValue(x);
}
writer.WriteEndArray();
}
}
writer.WriteEndObject();
}
}
You have a string, try json.Contains("'Type':'S'").
Then deserialize it to the proper model.

Categories

Resources