Reputation: 144
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.
Upvotes: 0
Views: 2781
Reputation: 144
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.
Upvotes: 1
Reputation: 45252
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)
};
}
Upvotes: 1