1

Consider the following 3rd party (Magento) API response where this is one entry in a collection of products:

{
   "id":121,
   "sku":"008MBLU",
   "name":"Pillow Covers - King Mauve (2-pack)",
   "custom_attributes":{
      "11":{
         "attribute_code":"ship_length",
         "value":"11.0000"
      },
      "16":{
         "attribute_code":"ship_width",
         "value":"7.0000"
      },
      "19":{
         "attribute_code":"ship_height",
         "value":"1.0000"
      }
   }
}

And the desired resulting class to deserialize to:

public class Product
{
    [JsonProperty("id")]
    public long Id { get; set; }

    [JsonProperty("sku")]
    public string SKU { get; set; }

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

    [JsonProperty("ship_length")]
    public decimal ShipLength { get; set; }

    [JsonProperty("ship_width")]
    public decimal ShipWidth { get; set; }

    [JsonProperty("ship_height")]
    public decimal ShipHeight { get; set; }
}

I found this post, which is part way what I need, which would ignore the higher level wrapper of the int values wrapping each custom_attribute. But I don't know where to start with regards to having a custom resolver that is for the custom_attribute property, and then assign it's value to another property....and I'm new to custom resolvers in general.

dbc
  • 104,963
  • 20
  • 228
  • 340
crichavin
  • 4,672
  • 10
  • 50
  • 95

1 Answers1

1

When reading the JSON corresponding to a Product, you need to restructure the "custom_attributes" properties up to the top level JSON object, where they can be recognized by the serializer. This can most easily be done with a custom JsonConverter that pre-loads the JSON into a JToken hierarchy, then restructures the hierarchy:

public class CustomAttributeObjectConverter<T> : JsonConverter<T> where T : new()
{
    public override T ReadJson(JsonReader reader, Type objectType, T existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        var obj = JToken.Load(reader).ToJTokenType<JObject>();
        if (obj == null)
            return (T)(object)null;
        var attributes = obj["custom_attributes"].RemoveFromLowestPossibleParent().ToJTokenType<JObject>();
        if (attributes != null)
        {
            foreach (var item in attributes.PropertyValues().OfType<JObject>())
            {
                var name = (string)item["attribute_code"];
                if (name != null)
                    obj.Add(name, item["value"]);
            }
        }
        if (!hasExistingValue)
            existingValue = (T)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
        using (var tokenReader = obj.CreateReader())
            serializer.Populate(tokenReader, existingValue);
        return existingValue;
    }

    public override void WriteJson(JsonWriter writer, T value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

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

public static partial class JsonExtensions
{
    public static TJToken ToJTokenType<TJToken>(this JToken item) where TJToken : JToken
    {
        var result = item as TJToken;
        if (item != null)
            return result;
        if (item == null || item.Type == JTokenType.Null)
            return null;
        throw new JsonException(string.Format("Cannot cast {0} to {1}", item.Type, typeof(TJToken)));
    }

    public static JToken RemoveFromLowestPossibleParent(this JToken node)
    {
        if (node == null)
            return null;
        // If the parent is a JProperty, remove that instead of the token itself.
        var contained = node.Parent is JProperty ? node.Parent : node;
        contained.Remove();
        // Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
        if (contained is JProperty)
            ((JProperty)node.Parent).Value = null;
        return node;
    }
}

Then deserialize as follows:

var settings = new JsonSerializerSettings
{
    Converters = { new CustomAttributeObjectConverter<Product>() },
};

var product = JsonConvert.DeserializeObject<Product>(json, settings);

Notes:

  • I did not attempt to implement WriteJson because there is no way to distinguish in a generic manner which properties should be demoted down into to the "custom_attributes" object while serializing.

    If this is a requirement, you could implement some custom attribute with which to mark the appropriate properties.

Demo fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • 1
    thanks for the completely thorough answer and providing the fiddle was awesome. I really appreciate all time you put into answering my question! – crichavin Oct 21 '19 at 19:02