Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JSON Array of Objects to Model in WebAPI using FromBody

I am creating a Web Api method that should accept a list of objects via XML or JSON and add them to a database.

Here is a very basic version of what I currently have:

[HttpPost]
public HttpResponseMessage Put([FromBody]ProductAdd productAdd)
{
    //do stuff with productadd object
    return Request.CreateResponse(HttpStatusCode.OK);
}

The model structure of the list of objects it accepts is as follows:

public class ProductAdd
{
    public List<ProductInformation> Products { get; set; }
}

public class ProductInformation
{
    public string ProductName { get; set; }
}

The above works perfectly when I am using XML - (Content-Type: application/xml)

<?xml version="1.0" encoding="utf-8"?>
<ProductAdd>
    <Products>  
        <ProductInformation>
            <ProductName>Seahorse Necklace</ProductName>
        </ProductInformation>
    </Products>
    <Products>  
        <ProductInformation>
            <ProductName>Ping Pong Necklace</ProductName>
        </ProductInformation>
    </Products>
</ProductAdd>

Products has 2 Items

But when I attempt to feed the same thing in using JSON (Content-Type: application/json), the Products list is empty

{
  "ProductAdd": {
    "Products": [
      {
        "ProductInformation": { "ProductName": "Seahorse Necklace" }
      },
      {
        "ProductInformation": { "ProductName": "Ping Pong Necklace" }
      }
    ]
  }
}

Products is null

Is there an issue with the JSON serializer when there is an array of objects within another object ?

Any ideas on what will fix this ?

Thanks

Edit: What serializers are you using for XML and Json? XML: XmlSerializer JSON: Newtonsoft

like image 521
strvanica Avatar asked Nov 21 '14 17:11

strvanica


2 Answers

The JSON you are sending to your Web API method does not match the structure you are deserializing into. Unlike XML, the root object in JSON does not have a name. You need to remove the wrapper object from your JSON to get it to work:

  {
    "Products": [
      {
        "ProductInformation": { "ProductName": "Seahorse Necklace" }
      },
      {
        "ProductInformation": { "ProductName": "Ping Pong Necklace" }
      }
    ]
  }

Alternatively, you could change your class structure to ADD a wrapper class, but then you would also need to change your XML to match that.

public class RootObject
{
    public ProductAdd ProductAdd { get; set; }
}
like image 187
Brian Rogers Avatar answered Sep 19 '22 18:09

Brian Rogers


In situations where deserialization is mysteriously failing, I find it helpful to serialize a test object and compare the actual output with the desired input. If the output differs from the desired input, that's probably the cause of the bug. In your case, if I deserialize the XML and re-serialize it to JSON, I get:

{
  "Products": [
    {
      "ProductName": "Seahorse Necklace"
    },
    {
      "ProductName": "Ping Pong Necklace"
    }
  ]
}

As you can see, there are two extra levels of indirection in your JSON as compared to the reserialized XML: there is a root object name, which should not be present in JSON, and there are collection element type names, which also should not be present. (Both of these are, however, common features in XML.)

Is it possible to change your JSON so that it doesn't have these extra levels if indirection? (E.g. was this JSON converted from XML via a script for testing purposes, and thus doesn't reflect real requirements?) If so, then your problem is solved.

If not, then here are some options for deserializing it:

  1. To read & write a root object name with you JSON, see the solutions here or here

    Or, roll your own proxy wrapper:

    public sealed class ProductAddWrapper
    {
        public ProductAddWrapper()
        {
        }
    
        public ProductAddWrapper(ProductAdd product)
        {
            this.ProductAdd = product;
        }
    
        public ProductAdd ProductAdd { get; set; }
    
        public static implicit operator ProductAdd(ProductAddWrapper wrapper) { return wrapper.ProductAdd; }
    
        public static implicit operator ProductAddWrapper(ProductAdd product) { return new ProductAddWrapper(product); }
    }
    
  2. Injecting the extra level of indirection into your lists is a bit more difficult. What you will need to do is to restructure the JSON on the fly as you read and write it, adding or removing the extra artificial level of nesting. This can be done with a JsonConverter:

    class CollectionWithNamedElementsConverter : JsonConverter
    {
        static Type GetEnumerableType(Type type)
        {
            foreach (Type intType in type.GetInterfaces())
            {
                if (intType.IsGenericType
                    && intType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
                {
                    return intType.GetGenericArguments()[0];
                }
            }
            return null;
        }
    
        public override bool CanConvert(Type objectType)
        {
            return typeof(IEnumerable).IsAssignableFrom(objectType)
                && !typeof(string).IsAssignableFrom(objectType)
                && GetEnumerableType(objectType) != null;
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            JArray originalArray = JArray.Load(reader);
            if (originalArray == null)
                return null;
            JArray array = new JArray();
            foreach (var item in originalArray)
            {
                var child = item.Children<JProperty>().FirstOrDefault();
                if (child != null)
                {
                    var value = child.Value;
                    array.Add(child.Value);
                }
            }
            return serializer.Deserialize(new StringReader(array.ToString()), objectType);
        }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var objectType = value.GetType();
            var itemType = GetEnumerableType(objectType);
    
            IEnumerable collection = value as IEnumerable;
    
            writer.WriteStartArray();
    
            foreach (object item in collection)
            {
                writer.WriteStartObject();
                writer.WritePropertyName(itemType.Name);
                serializer.Serialize(writer, item);
                writer.WriteEndObject();
            }
    
            writer.WriteEndArray();
        }
    }
    

Then use it by applying a [JsonConverter(typeof(CollectionWithNamedElementsConverter))] attribute to collections with item type names:

    public class ProductAdd
    {
        [JsonConverter(typeof(CollectionWithNamedElementsConverter))]
        public List<ProductInformation> Products { get; set; }
    }
like image 38
dbc Avatar answered Sep 17 '22 18:09

dbc