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.

0

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

1 Comment

thanks for the completely thorough answer and providing the fiddle was awesome. I really appreciate all time you put into answering my question!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.