Newtonsoft.JSON's ContractResolver conflict with custom converter

I have this sample hierarchical data:

{
  "Name": "Car 1",
  "AvailableColors": [
    "Red",
    "Green"
  ],
  "RelatedItems": {
    "Brand": {
      "Name": "Brand 1",
      "RelatedItems": {
        "ImportingCompanies": [
          {
            "Name": "Company 1",
            "RelatedItems": {
              "CeoName": "CEO 1"
            }
          },
          {
            "Name": "Company 2",
            "RelatedItems": {
              "CeoName": "CEO 2"
            }
          }
        ]
      }
    }
  }
}

Basically what I need is to copy all properties of any RelatedItems to its parent object. The result should become:

{
  "Name": "Car 1",
  "AvailableColors": [
    "Red",
    "Green"
  ],
  "Brand": {
    "Name": "Brand 1",
    "ImportingCompanies": [
      {
        "Name": "Company 1",
        "CeoName": "CEO 1"
      },
      {
        "Name": "Company 2",
        "CeoName": "CEO 2"
      }
    ]
  }
}

I'm using ASP.NET Core and I have configured my startup this way:

.AddNewtonsoftJson(options =>
{
    options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
    options.SerializerSettings.Converters.Add(new RelatedItemsFlattenerJsonConverter());
});

and this is the code I've written for RelatedItemsFlattenerJsonConverter:

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    var stopwatch = new Stopwatch();
    stopwatch.Start();
    JToken token = JToken.FromObject(value);
    if (token.Type == JTokenType.Array)
    {
        var array = (JArray)token;
        foreach (var item in token)
        {
            FlattenRelatedItems(item);
        }
    }
    else if (token.Type == JTokenType.Object)
    {
        var @object = (JObject)token;
        FlattenRelatedItems(@object);
    }
    token.WriteTo(writer);
    stopwatch.Stop();
}

private void FlattenRelatedItems(JToken token)
{
    if (token.Type == JTokenType.Array)
    {
        var array = (JArray)token;
        foreach (var item in token)
        {
            FlattenRelatedItems(item);
        }
    }
    else if (token.Type == JTokenType.Object)
    {
        var @object = (JObject)token;
        var relatedItemsProperty = @object.Properties().FirstOrDefault(i => i.Name.ToLower() == "RelatedItems".ToLower());
        if (relatedItemsProperty.IsNotNull())
        {
            var relatedItems = @object[relatedItemsProperty.Name];
            @object.Remove(relatedItemsProperty.Name);
            var keys = ((JObject)relatedItems).Properties().Select(i => i.Name).ToList();
            foreach (var key in keys)
            {
                @object.Add(key, relatedItems[key]);
            }
        }
        var properties = @object.Properties().ToList();
        foreach (var property in properties)
        {
            FlattenRelatedItems(@object[property.Name]);
        }
    }
}

It works like a charm. However, when I add the custom RelatedItemsFlattenerJsonConverter, my API response's casing would become PascalCased. And when I don't add it, the ContractResolver is respected and my API response is camelCased. What should I do to both have the custom converter, and camelCase for the API?

Upvotes: 1

Views: 667

Answers (1)

Brian Rogers
Brian Rogers

Reputation: 129707

When you call JToken.FromObject(value) inside WriteJson, you are not passing it the serializer, so it does not know about the CamelCasePropertyNamesContractResolver (or any other settings) you have configured. JToken.FromObject() uses a new serializer instance internally by default.

However, in this case, if you pass in the serializer as is, you may run into a self-referencing loop, as the converter attempts to call itself. What you need to do is create your own instance of JsonSerializer inside WriteJson and then copy the resolver reference (and any other important settings you want to keep, excluding the converter, of course) from the serializer that was passed to WriteJson. Then pass that new serializer to JToken.FromObject().

In other words, change this line:

JToken token = JToken.FromObject(value);

To this:

var innerSerializer = new JsonSerializer();
innerSerializer.ContractResolver = serializer.ContractResolver;
// if any other settings from the outer serializer are needed, copy them here
JToken token = JToken.FromObject(value, innerSerializer);

Upvotes: 2

Related Questions