Posting JSON Data to ASP.NET MVC

Im trying to get a list of line items to a webpage using JSON, which will then be manipulated and sent back to the server by ajax request using the same JSON structure that arrived (except having had a field values changed).

Receiving data from the server is easy, manipulation even easier! but sending that JSON data back to the server for saving... suicide time! PLEASE can someone help!

Javascript

var lineitems;

// get data from server
$.ajax({
    url: '/Controller/GetData/',
    success: function(data){
        lineitems = data;
    }
});

// post data to server
$.ajax({
    url: '/Controller/SaveData/',
    data: { incoming: lineitems }
});

C# - Objects

public class LineItem{
    public string reference;
    public int quantity;
    public decimal amount;
}

C# - Controller

public JsonResult GetData()
{
    IEnumerable<LineItem> lineItems = ... ; // a whole bunch of line items
    return Json(lineItems);
}

public JsonResult SaveData(IEnumerable<LineItem> incoming){
    foreach(LineItem item in incoming){
        // save some stuff
    }
    return Json(new { success = true, message = "Some message" });
}

The data arrives at the server as serialized post data. The automated model binder tries to bind IEnumerable<LineItem> incoming and surprisingly gets the resulting IEnumerable has the correct number of LineItems - it just doesnt populate them with data.

SOLUTION

Using answers from a number of sources, primarily djch on another stackoverflow post and BeRecursive below, I solved my problem using two main methods.

Server Side

The deserialiser below requires reference to System.Runtime.Serialization and using System.Runtime.Serialization.Json

private T Deserialise<T>(string json)
{
    using (var ms = new MemoryStream(Encoding.Unicode.GetBytes(json)))
    {
        var serialiser = new DataContractJsonSerializer(typeof(T));
        return (T)serialiser.ReadObject(ms);
    }
}

public void Action(int id, string items){
    IEnumerable<LineItem> lineitems = Deserialise<IEnumerable<LineItem>>(items);
    // do whatever needs to be done - create, update, delete etc.
}

Client Side

It uses json.org's stringify method, available in this dependecy https://github.com/douglascrockford/JSON-js/blob/master/json2.js (which is 2.5kb when minified)

$.ajax({
    type: 'POST',
    url: '/Controller/Action',
    data: { 'items': JSON.stringify(lineItems), 'id': documentId }
});

Take a look at Phil Haack's post on model binding JSON data. The problem is that the default model binder doesn't serialize JSON properly. You need some sort of ValueProvider OR you could write a custom model binder:

using System.IO;
using System.Web.Script.Serialization;

public class JsonModelBinder : DefaultModelBinder {
        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
            if(!IsJSONRequest(controllerContext)) {
                return base.BindModel(controllerContext, bindingContext);
            }

            // Get the JSON data that's been posted
            var request = controllerContext.HttpContext.Request;
            //in some setups there is something that already reads the input stream if content type = 'application/json', so seek to the begining
            request.InputStream.Seek(0, SeekOrigin.Begin);
            var jsonStringData = new StreamReader(request.InputStream).ReadToEnd();

            // Use the built-in serializer to do the work for us
            return new JavaScriptSerializer()
                .Deserialize(jsonStringData, bindingContext.ModelMetadata.ModelType);

            // -- REQUIRES .NET4
            // If you want to use the .NET4 version of this, change the target framework and uncomment the line below
            // and comment out the above return statement
            //return new JavaScriptSerializer().Deserialize(jsonStringData, bindingContext.ModelMetadata.ModelType);
        }

        private static bool IsJSONRequest(ControllerContext controllerContext) {
            var contentType = controllerContext.HttpContext.Request.ContentType;
            return contentType.Contains("application/json");
        }
    }

public static class JavaScriptSerializerExt {
        public static object Deserialize(this JavaScriptSerializer serializer, string input, Type objType) {
            var deserializerMethod = serializer.GetType().GetMethod("Deserialize", BindingFlags.NonPublic | BindingFlags.Static);

            // internal static method to do the work for us
            //Deserialize(this, input, null, this.RecursionLimit);

            return deserializerMethod.Invoke(serializer,
                new object[] { serializer, input, objType, serializer.RecursionLimit });
        }
    }

And tell MVC to use it in your Global.asax file:

ModelBinders.Binders.DefaultBinder = new JsonModelBinder();

Also, this code makes use of the content type = 'application/json' so make sure you set that in jquery like so:

$.ajax({
    dataType: "json",
    contentType: "application/json",            
    type: 'POST',
    url: '/Controller/Action',
    data: { 'items': JSON.stringify(lineItems), 'id': documentId }
});

The simplest way of doing this

I urge you to read this blog post that directly addresses your problem.

Using custom model binders isn't really wise as Phil Haack pointed out (his blog post is linked in the upper blog post as well).

Basically you have three options:

  1. Write a JsonValueProviderFactory and use a client side library like json2.js to communicate wit JSON directly.

  2. Write a JQueryValueProviderFactory that understands the jQuery JSON object transformation that happens in $.ajax or

  3. Use the very simple and quick jQuery plugin outlined in the blog post, that prepares any JSON object (even arrays that will be bound to IList<T> and dates that will correctly parse on the server side as DateTime instances) that will be understood by Asp.net MVC default model binder.

Of all three, the last one is the simplest and doesn't interfere with Asp.net MVC inner workings thus lowering possible bug surface. Using this technique outlined in the blog post will correctly data bind your strong type action parameters and validate them as well. So it is basically a win win situation.


In MVC3 they've added this.

But whats even more nice is that since MVC source code is open you can grab the ValueProvider and use it yourself in your own code (if youre not on MVC3 yet).

You will end up with something like this

ValueProviderFactories.Factories.Add(new JsonValueProviderFactory())