POST json dictionary
Asked Answered
L

11

25

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?

Lanceolate answered 17/1, 2011 at 6:56 Comment(1)
Possible duplicate of Posting JSON Data to ASP.NET MVCFumarole
J
15

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

Julissajulita answered 17/1, 2011 at 10:39 Comment(10)
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 ?Lanceolate
@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.Julissajulita
So think of it as a bug or an unimplemented feature. As you prefer :-)Julissajulita
Aah yes, I see your point now. I would call this a bug :). ThanksLanceolate
Bug posted (by someone else) connect.microsoft.com/VisualStudio/feedback/details/636647/…Fumarole
Worth noting that the above bug is now reported as FIXED at MS Connect. Yay for that.Aldine
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.Fumarole
Has anyone tried to model bind to a Dictionary<string, string> am using MVC5 and it doesn't work on my controllerIrkutsk
@Irkutsk I am facing the same problem. A dictionary nested within a class property is not being deserialized. Any news on this topic?Reproachless
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 helpsIrkutsk
F
22

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
}
Fumarole answered 22/3, 2011 at 20:57 Comment(0)
J
15

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

Julissajulita answered 17/1, 2011 at 10:39 Comment(10)
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 ?Lanceolate
@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.Julissajulita
So think of it as a bug or an unimplemented feature. As you prefer :-)Julissajulita
Aah yes, I see your point now. I would call this a bug :). ThanksLanceolate
Bug posted (by someone else) connect.microsoft.com/VisualStudio/feedback/details/636647/…Fumarole
Worth noting that the above bug is now reported as FIXED at MS Connect. Yay for that.Aldine
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.Fumarole
Has anyone tried to model bind to a Dictionary<string, string> am using MVC5 and it doesn't work on my controllerIrkutsk
@Irkutsk I am facing the same problem. A dictionary nested within a class property is not being deserialized. Any news on this topic?Reproachless
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 helpsIrkutsk
B
6

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

Betts answered 9/12, 2015 at 23:26 Comment(0)
W
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());
    }
Welloff answered 25/1, 2013 at 5:39 Comment(2)
Thank you very much for this solution. This worked for me when the other solutions posted here didn't.Azarria
Used this code pretty much verbatim (needed a Dictionary<string, object> instead) and it worked like a charm.Stypsis
D
2

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();
Dottiedottle answered 12/10, 2011 at 20:54 Comment(0)
V
2

Grab the following NuGet package for System.Json, which includes the new JsonValue type. JsonValue is a flexible new JSON representative type that fully supports C# 4 dynamic, and is also an IEnumerable<KeyValuePair<string, JsonValue>> in the event you wish to treat a payload as a dictionary/associative array.

You can pick up System.Json (Beta) with NuGet here. It seems System.Json will be included natively in .NET 4.5, as indicated by the documentation pages here.

You might also want to read the following article to assist in getting JSON HTTP bodies to properly deserialize into JsonValue objects in your Action method parameters:

JSON, ASP.NET MVC and JQuery - Working with Untyped JSON made easy

The two relevant pieces of code from the article above would be the DynamicJsonBinder and DynamicJsonAttribute, replicated here for posterity:

public class DynamicJsonBinder : IModelBinder  
{  
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)  
    { 
        if (!controllerContext.HttpContext.Request.ContentType.StartsWith  
              ("application/json", StringComparison.OrdinalIgnoreCase))  
        {  
            // not JSON request  
            return null;  
        }  

        var inpStream = controllerContext.HttpContext.Request.InputStream;  
        inpStream.Seek(0, SeekOrigin.Begin);  

        StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);  
        string bodyText = reader.ReadToEnd();  
        reader.Close();  


        if (String.IsNullOrEmpty(bodyText))  
        {  
            // no JSON data  
            return null;  
        }  

        return JsonValue.Parse(bodyText);  
    }  
} 

public class DynamicJsonAttribute : CustomModelBinderAttribute
{
    public override IModelBinder GetBinder()
    {
        return new DynamicJsonBinder();
    }
}

A relevant sample use case would be:

public class HomeController : Controller
{
    public ActionResult Index (T a)
    {
      return View();
    }

    public JsonResult A([DynamicJson] JsonValue value)
    {
      dynamic t = value.AsDynamic();

      if (t.Name.IsEmpty())
      {
        t = new // t is dynamic, so I figure just create the structure you need directly
        {
            Name = "myname",
            D = new // Associative array notation (woot!): 
            {
                a = "a",
                b = "b",
                c = "c" 
            }
        };
      }

      return Json(t);
    }
}
Vermis answered 21/6, 2012 at 23:0 Comment(0)
V
1

Just use a better deserializer. That first line where I set the position is because the JsonValueProvider leaves the stream at the end. More MS JSON fail.

Request.InputStream.Position = 0;
var reader = new StreamReader(Request.InputStream);

var model = Newtonsoft.Json.JsonConvert.DeserializeObject<CreativeUploadModel>(reader.ReadToEnd());

So somewhere in that CreativeUploadModel object graph there is a prop like this:

public Dictionary<string, Asset> Assets { get; set; }

Which is deserialized from (for example):

"assets":{"flash":{"type":"flash","value":"http://1234.cloudfront.net/1234.swf","properties":"{\"clickTag\":\"clickTAG\"}"}

Newtonsoft JSON is the default JSON provider for WebAPI... so it's not going anywhere.

Visigoth answered 30/4, 2012 at 5:53 Comment(0)
D
1

Here is my solution to similar problem:

using System.Collections.Generic;
using System.IO;
using System.Web.Mvc;
using System.Web.Script.Serialization;

namespace Controllers
{
    public class DictionaryModelBinder : IModelBinder
    {
        public object BindModel(ControllerContext context, ModelBindingContext bindingContext)
        {
            context.HttpContext.Request.InputStream.Seek(0, SeekOrigin.Begin);
            using (TextReader reader = new StreamReader(context.HttpContext.Request.InputStream))
            {
                string requestContent = reader.ReadToEnd();
                var arguments = new JavaScriptSerializer().Deserialize<Dictionary<string, object>>(requestContent);
                return arguments[bindingContext.ModelName];
            }
        }
    }
}

using Controllers;
using Moq;
using NUnit.Framework;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Web;
using System.Web.Mvc;

namespace ControllersTest
{
    [TestFixture]
    public class DictionaryModelBinderTest
    {
        private ControllerContext controllerContext;

        [Test]
        public void ReturnsDeserializedPrimitiveObjectsAndDictionaries()
        {
            string input =
@"{
    arguments: {
        simple: 1,
        complex: { a: 2, b: 3 },
        arrayOfSimple: [{ a: 4, b: 5 }],
        arrayOfComplex: [{ a: 6, b: 7 }, { a: 8, b: 9 }]},
    otherArgument: 1
}";
            SetUpRequestContent(input);

            var binder = new DictionaryModelBinder();
            var bindingContext = new ModelBindingContext();
            bindingContext.ModelName = "arguments";

            var model = (Dictionary<string, object>)binder.BindModel(controllerContext, bindingContext);

            Assert.IsFalse(model.ContainsKey("otherArgument"));
            Assert.AreEqual(1, model["simple"]);
            var complex = (Dictionary<string, object>)model["complex"];
            Assert.AreEqual(2, complex["a"]);
            Assert.AreEqual(3, complex["b"]);
            var arrayOfSimple = (ArrayList)model["arrayOfSimple"];
            Assert.AreEqual(4, ((Dictionary<string, object>)arrayOfSimple[0])["a"]);
            Assert.AreEqual(5, ((Dictionary<string, object>)arrayOfSimple[0])["b"]);
            var arrayOfComplex = (ArrayList)model["arrayOfComplex"];
            var complex1 = (Dictionary<string, object>)arrayOfComplex[0];
            var complex2 = (Dictionary<string, object>)arrayOfComplex[1];
            Assert.AreEqual(6, complex1["a"]);
            Assert.AreEqual(7, complex1["b"]);
            Assert.AreEqual(8, complex2["a"]);
            Assert.AreEqual(9, complex2["b"]);
        }

        private void SetUpRequestContent(string input)
        {
            var stream = new MemoryStream(Encoding.UTF8.GetBytes(input));
            stream.Seek(0, SeekOrigin.End);

            var controllerContextStub = new Mock<ControllerContext>();
            var httpContext = new Mock<HttpContextBase>();
            httpContext.Setup(x => x.Request.InputStream).Returns(stream);
            controllerContextStub.Setup(x => x.HttpContext).Returns(httpContext.Object);
            this.controllerContext = controllerContextStub.Object;
        }
    }
}

using Controllers;
using PortalApi.App_Start;
using System.Collections.Generic;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Routing;

namespace PortalApi
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            ModelBinders.Binders.Add(typeof(Dictionary<string, object>), new DictionaryModelBinder());
        }
    }
}

Have fun! :-P Greetings Łukasz Duda

Deportation answered 9/5, 2014 at 8:55 Comment(0)
I
1

Post the complex object as a string and deserialize at the other end. There is no type-safety however for this. Here is a dictionary with string key and string array values.

js:

var data = { 'dictionary': JSON.stringify({'A': ['a', 'b'] }) };

$.ajax({
    url: '/Controller/MyAction',
    data: JSON.stringify(data),
    type: 'POST',
    contentType: 'application/json',
    dataType: 'json'
});

c# controller:

[HttpPost]
public ActionResult MyAction(string dictionary)
{
    var s = new System.Web.Script.Serialization.JavaScriptSerializer();
    Dictionary<string, string[]> d = s.Deserialize<Dictionary<string, string[]>>(dictionary);
    return View();
}
Intarsia answered 23/9, 2014 at 20:28 Comment(0)
R
1

For anyone who's coming upon this problem recently still, as long as you don't need your controller to specifically accept a dictionary, you can do the following:

HttpResponseMessage SomeMethod([FromBody] IEnumerable<KeyValuePair<Key, Value>> values)
{
    Dictionary<Key, Value> dictionary = values.ToDictionary(x => x.Key, x = x.Value);
}

Though it is a bit hacky.

Repartition answered 8/2, 2016 at 21:29 Comment(1)
Missing '>'. should be: Dictionary<Key, Value> dictionary = values.ToDictionary(x => x.Key, x => x.Value);Cittern
M
0

If anyone will stumble upon this thread in 2023+ because it is the FIRST answer in Google - it works now out of a box (using .Net 8.0) even with Dictionary inside Dictionary.

Controller code:

    public IActionResult TestModelParsing( Dictionary<string, Dictionary<string, decimal>> ajaxData )
    {
        return Json( new {} );
    }

Javascript:

let data = {
    ajaxData: {
        "fieldName1": {
            innerField1: 1,
            innerField2: 2
        },
        "fieldName2": {
            innerField1: 1,
            innerField2: 2
        }
    }
}

$.ajax( {
    url: "/test/TestModelParsing",
    data: data,
    dataType: "json",
    method : "POST",
    timeout: 600000
} );

enter image description here

Mulholland answered 18/11, 2023 at 11:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.