0

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.

MkjG
  • 144
  • 2
  • 15
  • 1
    holup... why are we doing this in the first place `Task toConvert` Why do you need ta task here? – TheGeneral Aug 02 '20 at 23:50
  • The approach itself is questionable, even if it works with the provided answer. You can [read more about how to work with non-blocking code with Tasks](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/async-return-types) – insane_developer Aug 03 '20 at 00:11
  • @TheGeneral Consider caching collection of Task, in a Generic dictionary for arbitrary type Type, as per: stackoverflow.com/questions/36084495/when-to-cache-tasks (the code in question is from presentation by Stephen Toub).I want to provide a class that will sometimes provide callers with a future, but usually with a Task that has already been completed, in both ways exposing it as a Task. If caller awaits Task.Result, for cached tasks, it will return immedietelly. I then need to (de)serialize this cache, essentially ConcurrentDictionary>, but only saving T, not Tasks itself – MkjG Aug 03 '20 at 10:02

2 Answers2

1

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)
    };
}
Andrew Shepherd
  • 44,254
  • 30
  • 139
  • 205
  • This does not help my use case at all. Consider caching collection of Task, in a Generic dictionary for arbitrary type Type, as per: https://stackoverflow.com/questions/36084495/when-to-cache-tasks (the code in question is from presentation by Stephen Toub). I want to provide a class that will sometimes provide callers with a future, but usually with a Task that has already been completed, in both ways exposing it as a Task. If caller awaits Task.Result, for cached tasks, it will return immedietelly. I then need to (de)serialize this cache, essentially ConcurrentDictionary> – MkjG Aug 03 '20 at 09:51
  • And I don't know how to instruct json.net to (un)wrap TResult object from Task during (de)serialization, since TResult can be any generic type, and JsonConverter interface just passes me an 'object'. The code in Main is a calling code, I'd like to implement that functionality in class InQuestion, so that it serializes just the TResult (and omits from serialization uncompleted Tasks, which would be very rare) – MkjG Aug 03 '20 at 09:54
  • There is no way to serialize a task. If your use case requires you to do impossible things, I would take a step back and find out where you went wrong. – Lasse V. Karlsen Aug 03 '20 at 16:22
1

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.

MkjG
  • 144
  • 2
  • 15
  • 1
    Looking at this solution, it appears if the Task is not completed, you serialize a default instance of T instead. There will be no distinction between there being a default instance because it's uninitialized or there being a default instance because that's the actual correct value. – Andrew Shepherd Aug 03 '20 at 22:50