JamesNK / Newtonsoft.Json

Json.NET is a popular high-performance JSON framework for .NET
https://www.newtonsoft.com/json
MIT License
10.71k stars 3.24k forks source link

JsonConvert.SerializeObject not serializing tuple list in C# #2878

Open viveknuna opened 1 year ago

viveknuna commented 1 year ago

JsonConvert.SerializeObject is not working properly.restData has the property SuccessfulActivities and it has some values. But When I serialize it is serializing to an empty value for SuccessfulActivities. like the below. Its.Net 6 project. Please help.

{ "SuccessfulActivities": [ {} ], "Status": "{"Type":"INFO","Message":"OK"}", "RequestId": "bcb8b790-2cd8-4b2b-b05a-414a3c7ceb0b", "TenantId": "11111111-1111-1111-1111-84ba20252626" }

public string SerializeRestResponse(IRestResponse restData)
{
    JsonSerializerSettings jsonSettings = new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore, ContractResolver = new RestQueryResponseContractResolver() };
    return JsonConvert.SerializeObject(restData, jsonSettings);
}

public class RestQueryResponseContractResolver : DefaultContractResolver
{
    List<string> _properties;

    public RestQueryResponseContractResolver()
    {
        _properties = new List<string>();
        //_properties.Add(p.Name); This is just to show that I am adding other properties as well from different object type.
        PropertyInfo[] props2 = typeof(IRestBotResponse).GetProperties();
        foreach (PropertyInfo p in props2)
            _properties.Add(p.Name);
    }

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        JsonProperty property = base.CreateProperty(member, memberSerialization);

        property.ShouldSerialize = instance => _properties.Contains(property.PropertyName);

        return property;
    }
}

public interface IRestBotResponse : IRestResponse
{
    /// <summary>
    /// The list of successful conversation activities that were sent.
    /// For the Tuple:
    ///   Item1 is the user identifier (the user's Azure AD ID)
    ///   Item2 is the ActivityID
    /// </summary>
    [Required]
    List<(string, string)> SuccessfulActivities { get; }
}

I am able to get other properties properly. But it has a problem only with SuccessfulActivitiesand I think the reason is that SuccessfulActivitiesis a list of tupleList<(string, string)>.

I have read this answer, but it suggests using System.Text.Json, But I want to use NewtonSoft.Json as it's already being used in the project at many places. As it's already in production working project so I want to make changes with minimal side effects.

I am using this JSON response (responseJson) to send it to the client as the response of the HTTP request like this.

return new ContentResult()
{
    Content = responseJson,
    ContentType = "application/json; charset=utf-8",
    StatusCode = (int)statusCode,
};

I tried using the custom JsonConverter but it is also not considering the SuccessfulActivities. Please find below the relevant implementation. But it also didn't work. Then I tried commenting ContractResolver = new RestQueryResponseContractResolver() . I am getting the value of SuccessfulActivities. But I want to use ContractResolveras well because Its using RestQueryResponseContractResolver as the restDatahas many fields and We want to only return the selected fields and return them to the client.

public class RestBotResponse : RestResponse, IRestBotResponse
{
    public RestBotResponse()
    {
    }

    [JsonConstructor]
    public RestBotResponse(List<(string, string)> successfulActivities, string status, string tenantId, string requestId)
        : base(status, requestId, tenantId)
    {
        SuccessfulActivities = successfulActivities;
    }

    [Required]
    [JsonConverter(typeof(TupleListConverter<string, string>))]
    public List<(string, string)> SuccessfulActivities { get; set; }
}

public abstract class RestResponse : IRestResponse
{
    protected RestResponse()
    {
    }

    protected RestResponse(string status, string requestId, string tenantId)
    {
        Status = status;
        RequestId = requestId;
        if (tenantId == null)
            tenantId = string.Empty;
        TenantId = tenantId;
    }

    [Required]
    public string Status { get; set; }

    public string RequestId { get; set; }

    public string TenantId { get; set; }
}

public class TupleListConverter<T1, T2> : JsonConverter<List<(T1, T2)>>
{
    public override void WriteJson(JsonWriter writer, List<(T1, T2)> value, JsonSerializer serializer)
    {
        var array = value.Select(t => new { Item1 = t.Item1, Item2 = t.Item2 }).ToArray();
        serializer.Serialize(writer, array);
    }

    public override List<(T1, T2)> ReadJson(JsonReader reader, Type objectType, List<(T1, T2)> existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override bool CanRead => false;
}

Please find the implementation of RestQueryResponseContractResolver

public class RestQueryResponseContractResolver : DefaultContractResolver
{
    List<string> _properties;

    public RestQueryResponseContractResolver()
    {
        _properties = new List<string>();

        // Include all ITeamUserInfo properties
        PropertyInfo[] teamUserInfoProps = typeof(ITeamUserInfo).GetProperties();
        foreach (PropertyInfo p in teamUserInfoProps)
            _properties.Add(p.Name);

        // Include all RestQueryResponse properties
        PropertyInfo[] restResponseProps = typeof(RestQueryResponse).GetProperties();
        foreach (PropertyInfo p in restResponseProps)
            _properties.Add(p.Name);

        //PropertyInfo[] props2 = typeof(IRestBotResponse).GetProperties();
        //foreach (PropertyInfo p in props2)
        //    _properties.Add(p.Name);
    }

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        JsonProperty property = base.CreateProperty(member, memberSerialization);

        property.ShouldSerialize = instance => _properties.Contains(property.PropertyName);

        return property;
    }
}

Newtonsoft.json version: 13.0.2

I have also posted the question on StackOverflow, but couldn't get enough of support so posting here for better reach.

elgonzo commented 1 year ago

I am not going to debug all your code (i am sorry), but note that Newtonsoft.Json has no problems serializing List<(string, string)> as evidenced by this fiddle (using Newtonsoft.Json 13.0.2) : https://dotnetfiddle.net/tVUZBN

However, you seem to misunderstand what a ContractResolver does. The configured ContractResolver will be used to obtain serialization contracts for every type encountered during serialization. Let me repeat that: for every type.

Your RestQueryResponseContractResolver is not only providing contracts for some RestQueryResponse instances, but also for string instances, for List\<(string, string)> and, most importantly here regarding your problem, also for (string, string) tuple instances.

Why does that matter? Because the CreateProperty implementation of your ContractResolver doesn't check and therefore doesn't care about the type for which a contract is being created when CreateProperty method is being called, and applies the ShouldSerialize condition to any member of any type being involved in (de)serialization that will be represented as a json object.[1]

And (string, string) will be serialized as json object. Once more, understand that CreateProperty will not only be called for members of your RestQueryResponse type(s), but also for members belonging to any other type that will be represented as a json object, and that includes the members (fields) of (string, string).

So, your RestQueryResponseContractResolver is going to create a contract for (string, string), and CreateProperty will be called for every member of (string, string), which are the fields named "Item1" and "Item2". But since neither member of (string, string) has a name matching the name of the properties in IRestBotResponse, your contract resolver unceremoniously disables serialization for these (string, string) members via:

    property.ShouldSerialize = instance => _properties.Contains(property.PropertyName);

leading to (string, string) instances being serialized as empty json objects. Kaboom!


It should now be clear how to make your contract resolver well behave: Set property.ShouldSerialize only for members which belong to IRestBotResponse or derived types.


[1] CreateProperty will not be called for members belonging to a type that will be serialized as a Json array or some "primitive" json value like number, string or boolean. That's why the string values and the List<T> itself in your RestQueryResponse instances serialize just fine, as they aren't serialized as json object and thuse don't lead to CreateProperty being invoked for them.


P.S.: I am not the author nor a maintainer of Newtonsoft.Json nor am i associated with them in any way. I am just a user of Newtonsoft.Json...

viveknuna commented 1 year ago

@elgonzo Thank you for the detailed info But the question remains unanswered. How can I solve the issue? What is the correct way to handle this particular case?

elgonzo commented 1 year ago

Did you read my post to the end? I spelled out how to solve the issue...

viveknuna commented 1 year ago

@elgonzo I still dint get it. Can you show me with code? FYI, restData has other properties as well.

elgonzo commented 1 year ago

As i wrote at the end of my post:

It should now be clear how to make your contract resolver well behave: Set property.ShouldSerialize only for members which belong to IRestBotResponse or derived types.

viveknuna commented 1 year ago

@elgonzo I tried but still the same issue.

elgonzo commented 1 year ago

This is the issue tracker of Newtonsoft.Json.

As i have shown in the dotnetfiddle linked to in my first post, Newtonsoft.Json does serialize (string, string) tuples and lists of (string, string) tuples. I furthermore explained how Newtonsoft.Json's contract resolvers operate, because frankly, Newtonsoft.Json's official documentation is rather sparse on that topic.

I even told you which single specific code line in your contract resolver is the cause of your issues. I don't know what you tried, and frankly i don't care. What else would you need more than to be pointed at the very exact code line that causes the issue? If you struggle with your own code, stackoverflow.com is a better place to ask and discuss, not Newtonsoft.Jsons' bug tracker. It's a shame that Newtonsoft.Json's github hasn't the discussion area enabled, but it is what it is.

And just to be clear, the dotnetfiddle showing that Newtonsoft.Json can serialize lists of value tuples like List\<(string, string)> without needing some converter should make it more than obvious that the TupleListConverter featured in your report is a completely useless and unnecessary distraction from the real problem that lies in the RestQueryResponseContractResolver implementation as posted in your issue report.

As i said in the very beginning of my first post:

I am not going to debug all your code (i am sorry) [..]

That's it...

viveknuna commented 1 year ago

@elgonzo your answer didn’t help me at all, and one suggestion, be humble and learn to control yours words.

sungam3r commented 1 year ago

@elgonzo I have been watching your answers for a very long time. It’s good when there is such a person in repo ready to help with advice. Do not take what is happening close to your heart.

@viveknuna No one can help you if you yourself do not allow.

elgonzo commented 1 year ago

@viveknuna

@elgonzo your answer didn’t help me at all

That's okay. I specifically pointed out the cause of your problem and explained how to remedy it (i quoted that part again in a second comment), and you of course are free and entitled to ignore and dismiss it.

be humble and learn to control yours words.

I tried to help you and i clearly pointed out my limits to what i am willing to do in this space here (Newtonsoft.Json's issue tracker). While i understand and accept that you might be unhappy with that, i note that you are trying to lecture me about humbleness, one who was willing to spend their own time to help you understand and fix your problem. Does this kind of ad hominem work with other people?

But you know what i really learned here? To ignore you if i ever will come across another of your questions or issue reports. That's not the lesson i was looking forward to learn, but it's a lesson i neverless learned.