POST json dictionary
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):
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();
Related videos on Youtube
Comments
-
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?
-
Chris Moschini about 8 yearsPossible duplicate of Posting JSON Data to ASP.NET MVC
-
-
sirrocco over 13 yearsCare 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 over 13 years@sirrocco, it does more than this. Look at the
JsonValueProviderFactory
with reflector. You will see that it uses theDeserializeObject
method instead ofDeserialize
because at that moment it doesn't know the type of the model. Then it builds a completely newDictionaryValueProvider
and as you can see only theMakePropertyKey
andMakeArrayKey
private functions are implemented which generate theprefix.propertyName
andprefix[index]
notation. There is nothing that handles the case of a dictionary which need to be of the formprefix[index].Key
andprefix[index].Value
. -
Darin Dimitrov over 13 yearsSo think of it as a bug or an unimplemented feature. As you prefer :-)
-
sirrocco over 13 yearsAah yes, I see your point now. I would call this a bug :). Thanks
-
Chris Moschini about 13 yearsBug posted (by someone else) connect.microsoft.com/VisualStudio/feedback/details/636647/…
-
John Hargrove almost 13 yearsWorth noting that the above bug is now reported as FIXED at MS Connect. Yay for that.
-
Chris Moschini over 11 yearsAnd 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 about 10 yearsThank you very much for this solution. This worked for me when the other solutions posted here didn't.
-
Marc Chu about 10 yearsUsed this code pretty much verbatim (needed a Dictionary<string, object> instead) and it worked like a charm.
-
Luiso over 8 yearsHas anyone tried to model bind to a Dictionary<string, string> am using MVC5 and it doesn't work on my controller
-
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 over 8 yearsafter 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