1
votes

My requirement is use JsonProperty during de-serializing and ignore JsonProperty during serialization. My model,

[JsonConverter(typeof(JsonPathConverter))]
public class FacebookFeed
{
    public FacebookFeed()
    {
        Posts = new List<FacebookFeedPost>();
    }

    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("fan_count")]
    public int Likes { get; set; }

    [JsonProperty("feed.data")]
    public List<FacebookFeedPost> Posts { get; set; }
}

public class FacebookFeedPost
{
    [JsonProperty("id")]
    public string Id { get; set; }

    [JsonProperty("message")]
    public string Message { get; set; }

    [JsonProperty("created_time")]
    public DateTime Date { get; set; }

    [JsonProperty("comments.summary.total_count")]
    public int Comments { get; set; }        
}

class IgnoreJsonPropertyContractResolver : DefaultContractResolver
{
    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        var list = base.CreateProperties(type, memberSerialization);
        foreach (JsonProperty prop in list)
        {
            prop.PropertyName = prop.UnderlyingName;
        }
        return list;
    }
}

public class JsonPathConverter : JsonConverter
{
    public override object ReadJson(JsonReader reader, Type objectType,
                                    object existingValue, JsonSerializer serializer)
    {
        var jo = JObject.Load(reader);
        object targetObj = Activator.CreateInstance(objectType);

        foreach (var prop in objectType.GetProperties()
                                                .Where(p => p.CanRead && p.CanWrite))
        {
            var att = prop.GetCustomAttributes(true).OfType<JsonPropertyAttribute>().FirstOrDefault();
            var jsonPath = (att != null ? att.PropertyName : prop.Name);
            var token = jo.SelectToken(jsonPath);
            if (token != null && token.Type != JTokenType.Null)
            {
                var value = token.ToObject(prop.PropertyType, serializer);
                prop.SetValue(targetObj, value, null);
            }
        }

        return targetObj;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var s = new JsonSerializer();
        serializer.ContractResolver = new IgnoreJsonPropertyContractResolver();
        var t = JToken.FromObject(value, s);
        t.WriteTo(writer);
    }

    public override bool CanConvert(Type objectType)
    {
        // CanConvert is not called when [JsonConverter] attribute is used
        return false;
    }

    public override bool CanWrite
    {
        get { return true; }
    }

 }

The problem is WriteJson. Note that I am using ASP.NET Web Api which internally serialize my objects that's why I am using JsonConverter. I dont wanna change global web api settings.

1
Where is the exception raised exactly? Do you have the call stack?themiurge
var t = JToken.FromObject(value, s); at this line. No Stack trace.Imran Qadir Baksh - Baloch
Your problem is that the converter is calling itself recursively. Since it is applied directly to the type, the inner call to JToken.FromObject(value, s); will call the WriteJson() of a 2nd instance of the converter. One possibility to avoid this is shown in JSON.Net throws StackOverflowException when using [JsonConvert()].dbc
It might just be easier for you to use a custom contract resolver and manually serialize the response yourself following the instructions in Set JsonSerializerSettings Per Response?.dbc

1 Answers

3
votes

I think you have an XY Problem going on here.

I think the real problem you are trying to solve is that you have some deeply nested JSON which you want to deserialize into a simpler, flattened model, and then you want to be able to serialize that simpler model to JSON.

You've found a possible solution for the deserialization part, a converter which overloads the [JsonProperty] attribute to accept a path for each property, making it easier to flatten to a simpler model. You've used a [JsonConverter] attribute to apply the converter to your class because you don't want to modify the global settings for Web API.

But now, when you serialize, the [JsonProperty] names (paths) are getting picked up by the serializer, which you don't want. So you needed a way to ignore them. You then found a possible solution for that, involving using a custom contract resolver to revert the [JsonProperty] names back to their original values. You are trying to apply the resolver inside the converter's WriteJson method, but when you try to serialize your object inside the converter, the converter gets called recursively due to the fact that the model class has a [JsonConverter] attribute on it. So now you're stuck, and you're asking how to get around this latest problem. Am I right so far?

OK, so let's back up a few steps and solve the real problem. You want to deserialize a deeply nested JSON into a simple model and then serialize that simple model to JSON. I think you are on the right track using a converter to flatten the JSON down on deserialization, but there's no reason that the converter has to hijack the [JsonProperty] attribute to do it. It could instead use its own custom attribute for the JSON paths so as not to interfere with the normal operation of the serializer. If you do that, then you can make the converter's CanWrite method return false, which will cause the serializer to ignore the converter and use the default property names, which is what you wanted in the first place.

So, here is what you need to do:

First, make a custom attribute class to use for the property paths:

public class JsonPathAttribute : Attribute
{
    public JsonPathAttribute(string jsonPath)
    {
        JsonPath = jsonPath;
    }

    public string JsonPath { get; set; }
}

Next, change the ReadJson method of your converter to look for this new attribute instead of [JsonProperty], and make the CanWrite method return false. You can also get rid of the implementation for the WriteJson method, as it will never be called. The custom resolver class is not needed either.

public class JsonPathConverter : JsonConverter
{
    public override object ReadJson(JsonReader reader, Type objectType,
                                    object existingValue, JsonSerializer serializer)
    {
        var jo = JObject.Load(reader);
        object targetObj = Activator.CreateInstance(objectType);

        foreach (var prop in objectType.GetProperties()
                                       .Where(p => p.CanRead && p.CanWrite))
        {
            var att = prop.GetCustomAttributes(true)
                          .OfType<JsonPathAttribute>()
                          .FirstOrDefault();
            var jsonPath = (att != null ? att.JsonPath : prop.Name);
            var token = jo.SelectToken(jsonPath);
            if (token != null && token.Type != JTokenType.Null)
            {
                var value = token.ToObject(prop.PropertyType, serializer);
                prop.SetValue(targetObj, value, null);
            }
        }

        return targetObj;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        // WriteJson is not called when CanWrite returns false
    }

    public override bool CanWrite
    {
        get { return false; }
    }

    public override bool CanConvert(Type objectType)
    {
        // CanConvert is not called when [JsonConverter] attribute is used
        return false;
    }
}

Finally, change your model classes to use the new attribute. Note that you will also need to mark both classes with the [JsonConverter] attribute; in your question you only marked the first one.

[JsonConverter(typeof(JsonPathConverter))]
public class FacebookFeed
{
    public FacebookFeed()
    {
        Posts = new List<FacebookFeedPost>();
    }

    [JsonPath("name")]
    public string Name { get; set; }

    [JsonPath("fan_count")]
    public int Likes { get; set; }

    [JsonPath("feed.data")]
    public List<FacebookFeedPost> Posts { get; set; }
}

[JsonConverter(typeof(JsonPathConverter))]
public class FacebookFeedPost
{
    [JsonPath("id")]
    public string Id { get; set; }

    [JsonPath("message")]
    public string Message { get; set; }

    [JsonPath("created_time")]
    public DateTime Date { get; set; }

    [JsonPath("comments.summary.total_count")]
    public int Comments { get; set; }
}

And that's it. It should now work the way you want.

Demo fiddle: https://dotnetfiddle.net/LPPAmH