POST json dictionary

44,524

Solution 1

Due to the way JsonValueProviderFactory is implemented binding dictionaries is not supported.

Solution 2

An unfortunate workaround:

data.dictionary = {
    'A': 'a',
    'B': 'b'
};

data.dictionary = JSON.stringify(data.dictionary);

. . .

postJson('/mvcDictionaryTest', data, function(r) {
    debugger;
}, function(a,b,c) {
    debugger;
});

postJSON js lib function (uses jQuery):

function postJson(url, data, success, error) {
    $.ajax({
        url: url,
        data: JSON.stringify(data),
        type: 'POST',
        contentType: 'application/json; charset=utf-8',
        dataType: 'json',
        success: success,
        error: error
    });
}

The ViewModel object being posted (presumably has a lot more going on than a dictionary):

public class TestViewModel
{
    . . .
    //public Dictionary<string, string> dictionary { get; set; }
    public string dictionary { get; set; }
    . . .
}

The Controller method being posted to:

[HttpPost]
public ActionResult Index(TestViewModel model)
{
    var ser = new System.Web.Script.Serialization.JavascriptSerializer();
    Dictionary<string, string> dictionary = ser.Deserialize<Dictionary<string, string>>(model.dictionary);

    // Do something with the dictionary
}

Solution 3

Using ASP.NET 5 and MVC 6 straight out of the box I'm doing this:

jSON:

{
    "Name": "somename",
    "D": {
        "a": "b",
        "b": "c",
        "c": "d"
    }
}

Controller:

[HttpPost]
public void Post([FromBody]Dictionary<string, object> dictionary)
{
}

This is what shows up when it comes through (Name and D are the keys):

enter image description here

Solution 4

I came across the same issue today and came up with a solution which doesn't require anything but registering a new model binder. It's a bit hacky but hopefully it helps someone.

    public class DictionaryModelBinder : IModelBinder
    {
        /// <summary>
        /// Binds the model to a value by using the specified controller context and binding context.
        /// </summary>
        /// <returns>
        /// The bound value.
        /// </returns>
        /// <param name="controllerContext">The controller context.</param><param name="bindingContext">The binding context.</param>
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
                throw new ArgumentNullException("bindingContext");

            string modelName = bindingContext.ModelName;
            // Create a dictionary to hold the results
            IDictionary<string, string> result = new Dictionary<string, string>();

            // The ValueProvider property is of type IValueProvider, but it typically holds an object of type ValueProviderCollect
            // which is a collection of all the registered value providers.
            var providers = bindingContext.ValueProvider as ValueProviderCollection;
            if (providers != null)
            {
                // The DictionaryValueProvider is the once which contains the json values; unfortunately the ChildActionValueProvider and
                // RouteDataValueProvider extend DictionaryValueProvider too, so we have to get the provider which contains the 
                // modelName as a key. 
                var dictionaryValueProvider = providers
                    .OfType<DictionaryValueProvider<object>>()
                    .FirstOrDefault(vp => vp.ContainsPrefix(modelName));
                if (dictionaryValueProvider != null)
                {
                    // There's no public property for getting the collection of keys in a value provider. There is however
                    // a private field we can access with a bit of reflection.
                    var prefixsFieldInfo = dictionaryValueProvider.GetType().GetField("_prefixes",
                                                                                      BindingFlags.Instance |
                                                                                      BindingFlags.NonPublic);
                    if (prefixsFieldInfo != null)
                    {
                        var prefixes = prefixsFieldInfo.GetValue(dictionaryValueProvider) as HashSet<string>;
                        if (prefixes != null)
                        {
                            // Find all the keys which start with the model name. If the model name is model.DictionaryProperty; 
                            // the keys we're looking for are model.DictionaryProperty.KeyName.
                            var keys = prefixes.Where(p => p.StartsWith(modelName + "."));
                            foreach (var key in keys)
                            {
                                // With each key, we can extract the value from the value provider. When adding to the dictionary we want to strip
                                // out the modelName prefix. (+1 for the extra '.')
                                result.Add(key.Substring(modelName.Length + 1), bindingContext.ValueProvider.GetValue(key).AttemptedValue);
                            }
                            return result;
                        }
                    }
                }
            }
            return null;
        }
    }

The binder is registered in the Global.asax file under application_start

    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();

        RegisterGlobalFilters(GlobalFilters.Filters);
        RegisterRoutes(RouteTable.Routes);

        ModelBinders.Binders.Add(typeof(Dictionary<string,string>), new DictionaryModelBinder());
    }

Solution 5

I got it to work with a custom model binder, and changing the way the data is sent; without using Stringify and setting the contenttype.

JavaScript:

    $(function() {
        $.ajax({
            url: '/home/a',
            type: 'POST',
            success: function(result) {
                $.ajax({
                    url: '/home/a',
                    data: result,
                    type: 'POST',
                    success: function(result) {

                    }
                });
            }
        });
    });

Custom model binder:

public class DictionaryModelBinder : IModelBinder
{          
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
            throw new ArgumentNullException("bindingContext");

        string modelName = bindingContext.ModelName;
        IDictionary<string, string> formDictionary = new Dictionary<string, string>();

        Regex dictionaryRegex = new Regex(modelName + @"\[(?<key>.+?)\]", RegexOptions.CultureInvariant);
        foreach (var key in controllerContext.HttpContext.Request.Form.AllKeys.Where(k => k.StartsWith(modelName + "[")))
        {
            Match m = dictionaryRegex.Match(key);
            if (m.Success)
            {
                formDictionary[m.Groups["key"].Value] = controllerContext.HttpContext.Request.Form[key];
            }
        }
        return formDictionary;
    }
}

And by adding the model binder in Global.asax:

ModelBinders.Binders[typeof(IDictionary<string, string>)] = new DictionaryModelBinder();
Share:
44,524

Related videos on Youtube

sirrocco
Author by

sirrocco

--

Updated on July 09, 2022

Comments

  • sirrocco
    sirrocco almost 2 years

    I'm trying the following : A model with a dictionary inside send it on the first ajax request then take the result serialize it again and send it back to the controller.

    This should test that I can get back a dictionary in my model. It doesn't work

    Here's my simple test:

    public class HomeController : Controller
    {
        public ActionResult Index (T a)
        {
          return View();
        }
    
        public JsonResult A(T t)
        {
          if (t.Name.IsEmpty())
          {
            t = new T();
            t.Name = "myname";
            t.D = new Dictionary<string, string>();
            t.D.Add("a", "a");
            t.D.Add("b", "b");
            t.D.Add("c", "c");
          }
          return Json(t);
        }
    }
    
    //model
    public class T
    {
      public string Name { get; set; }
      public IDictionary<string,string> D { get; set; }
    }
    

    The javascript:

    $(function () {
        var o = {
            Name: 'somename',
            "D": {
                "a": "b",
                "b": "c",
                "c": "d"
            }
        };
    
        $.ajax({
            url: actionUrl('/home/a'),
            contentType: 'application/json',
            type: 'POST',
            success: function (result) {
                $.ajax({
                    url: actionUrl('/home/a'),
                    data: JSON.stringify(result),
                    contentType: 'application/json',
                    type: 'POST',
                    success: function (result) {
                    }
                });
            }
        });
    });
    

    In firebug the json received and the json sent are identical. I can only assume something gets lost on the way.

    Anyone has an idea as to what I'm doing wrong?

  • sirrocco
    sirrocco over 13 years
    Care to elaborate more on that ? I mean it just reads the input stream and passes it to the JavascriptSerializer. Does it do anything else weird ?
  • Darin Dimitrov
    Darin Dimitrov over 13 years
    @sirrocco, it does more than this. Look at the JsonValueProviderFactory with reflector. You will see that it uses the DeserializeObject method instead of Deserialize because at that moment it doesn't know the type of the model. Then it builds a completely new DictionaryValueProvider and as you can see only the MakePropertyKey and MakeArrayKey private functions are implemented which generate the prefix.propertyName and prefix[index] notation. There is nothing that handles the case of a dictionary which need to be of the form prefix[index].Key and prefix[index].Value.
  • Darin Dimitrov
    Darin Dimitrov over 13 years
    So think of it as a bug or an unimplemented feature. As you prefer :-)
  • sirrocco
    sirrocco over 13 years
    Aah yes, I see your point now. I would call this a bug :). Thanks
  • Chris Moschini
    Chris Moschini about 13 years
  • John Hargrove
    John Hargrove almost 13 years
    Worth noting that the above bug is now reported as FIXED at MS Connect. Yay for that.
  • Chris Moschini
    Chris Moschini over 11 years
    And confirmed - if you upgrade to ASP.Net 4.5 and MVC 4 you can serialize JSON Dictionaries on POST via the default ValueBinders in MVC.
  • Andy
    Andy about 10 years
    Thank you very much for this solution. This worked for me when the other solutions posted here didn't.
  • Marc Chu
    Marc Chu about 10 years
    Used this code pretty much verbatim (needed a Dictionary<string, object> instead) and it worked like a charm.
  • Luiso
    Luiso over 8 years
    Has anyone tried to model bind to a Dictionary<string, string> am using MVC5 and it doesn't work on my controller
  • Steven Ryssaert
    Steven Ryssaert over 8 years
    @Luiso I am facing the same problem. A dictionary nested within a class property is not being deserialized. Any news on this topic?
  • Luiso
    Luiso over 8 years
    after a while i realized that the issue was that on my json i had a colon after the last <key, value> pair, something like this {"data": "value",} which is invalid json btw. Once I fixed that it worked just fine. Hope this helps