How to use Json.NET for JSON modelbinding in an MVC5 project?
Asked Answered
T

3

29

I've been looking around the internet for an answer or example, but could not find one yet. I simply would like to change the default JSON serializer which is used to deserialize JSON while modelbinding to JSON.NET library.

I've found this SO post, but cannot implement it so far, I can't even see the System.Net.Http.Formatters namespace, nor can I see GlobalConfiguration.

What am I missing?

UPDATE

I have an ASP.NET MVC project, it was basically an MVC3 project. Currently I'm targetting .NET 4.5 and using the ASP.NET MVC 5 and related NuGet packages.

I don't see the System.Web.Http assembly, nor any similar namespace. In this context I would like to inject JSON.NET to be used as the default model binder for JSON type of requests.

Teletype answered 2/6, 2014 at 13:2 Comment(2)
GlobalConfiguration -> msdn.microsoft.com/en-us/library/…Craggy
Yes, I've seen that, but I can't add reference to System.Web.Http because it's not in the reference list. Added some more information to my question.Magdalenmagdalena
T
30

I've finally found an answer. Basically I don't need the MediaTypeFormatter stuff, that's not designed to be used in MVC environment, but in ASP.NET Web APIs, that's why I do not see those references and namespaces (by the way, those are included in the Microsoft.AspNet.WeApi NuGet package).

The solution is to use a custom value provider factory. Here is the code required.

    public class JsonNetValueProviderFactory : ValueProviderFactory
    {
        public override IValueProvider GetValueProvider(ControllerContext controllerContext)
        {
            // first make sure we have a valid context
            if (controllerContext == null)
                throw new ArgumentNullException("controllerContext");

            // now make sure we are dealing with a json request
            if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
                return null;

            // get a generic stream reader (get reader for the http stream)
            var streamReader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
            // convert stream reader to a JSON Text Reader
            var JSONReader = new JsonTextReader(streamReader);
            // tell JSON to read
            if (!JSONReader.Read())
                return null;

            // make a new Json serializer
            var JSONSerializer = new JsonSerializer();
            // add the dyamic object converter to our serializer
            JSONSerializer.Converters.Add(new ExpandoObjectConverter());

            // use JSON.NET to deserialize object to a dynamic (expando) object
            Object JSONObject;
            // if we start with a "[", treat this as an array
            if (JSONReader.TokenType == JsonToken.StartArray)
                JSONObject = JSONSerializer.Deserialize<List<ExpandoObject>>(JSONReader);
            else
                JSONObject = JSONSerializer.Deserialize<ExpandoObject>(JSONReader);

            // create a backing store to hold all properties for this deserialization
            var backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
            // add all properties to this backing store
            AddToBackingStore(backingStore, String.Empty, JSONObject);
            // return the object in a dictionary value provider so the MVC understands it
            return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
        }

        private static void AddToBackingStore(Dictionary<string, object> backingStore, string prefix, object value)
        {
            var d = value as IDictionary<string, object>;
            if (d != null)
            {
                foreach (var entry in d)
                {
                    AddToBackingStore(backingStore, MakePropertyKey(prefix, entry.Key), entry.Value);
                }
                return;
            }

            var l = value as IList;
            if (l != null)
            {
                for (var i = 0; i < l.Count; i++)
                {
                    AddToBackingStore(backingStore, MakeArrayKey(prefix, i), l[i]);
                }
                return;
            }

            // primitive
            backingStore[prefix] = value;
        }

        private static string MakeArrayKey(string prefix, int index)
        {
            return prefix + "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
        }

        private static string MakePropertyKey(string prefix, string propertyName)
        {
            return (String.IsNullOrEmpty(prefix)) ? propertyName : prefix + "." + propertyName;
        }
    }

And you can use it like this in your Application_Start method:

// remove default implementation    
ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<JsonValueProviderFactory>().FirstOrDefault());
// add our custom one
ValueProviderFactories.Factories.Add(new JsonNetValueProviderFactory());

Here is the post which pointed me to the right direction, and also this one gave a good explanation on value providers and modelbinders.

Teletype answered 3/6, 2014 at 9:50 Comment(4)
also mentioned here from #14592250Lucius
I used a slightly modified version of this code, and on an action with a single string input, like public ResultObject DoThis(string input) the line JSONReader.Read dies with an exception like "Inappropriate character, the text isn't JSON". Any thoughts?Lucius
@Lucius to answer my question, it seems that a form-data encoded request, rather than a raw JSON (via POSTMAN), will trigger my issue, since it tries to deserialize essentially gibberishLucius
Using this seems to break the ability to pass in enum values by their integer values (e.g. {someEnum:2}). Using the EnumModelBinder class from here fixes it again.Bushwhack
T
25

I had such a problem with this as well. I was posting JSON to an action, yet my JsonProperty names were ignored. Thus, my model properties were always empty.

public class MyModel
{
    [JsonProperty(PropertyName = "prop1")]
    public int Property1 { get; set; }

    [JsonProperty(PropertyName = "prop2")]
    public int Property2 { get; set; }

    [JsonProperty(PropertyName = "prop3")]
    public int Property3 { get; set; }

    public int Foo { get; set; }
}

I am posting to an action using this custom jquery function:

(function ($) {
    $.postJSON = function (url, data, dataType) {

        var o = {
            url: url,
            type: 'POST',
            contentType: 'application/json; charset=utf-8'
        };

        if (data !== undefined)
            o.data = JSON.stringify(data);

        if (dataType !== undefined)
            o.dataType = dataType;

        return $.ajax(o);
    };
}(jQuery));

And I call it like this:

data = {
    prop1: 1,
    prop2: 2,
    prop3: 3,
    foo: 3,
};

$.postJSON('/Controller/MyAction', data, 'json')
            .success(function (response) {
                ...do whatever with the JSON I got back
            });

Unfortunately, only foo was ever getting bound (odd, since the case is not the same, but I guess the default modelbinder isn't case-sensitive)

[HttpPost]
public JsonNetResult MyAction(MyModel model)
{
    ...
}

The solution ended up being rather simple

I just implemented a generic version of Dejan's model binder which works very nicely for me. It could probably use some dummy checks (like making sure the request is actually application/json), but it's doing the trick right now.

internal class JsonNetModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        controllerContext.HttpContext.Request.InputStream.Position = 0;
        var stream = controllerContext.RequestContext.HttpContext.Request.InputStream;
        var readStream = new StreamReader(stream, Encoding.UTF8);
        var json = readStream.ReadToEnd();
        return JsonConvert.DeserializeObject(json, bindingContext.ModelType);
    }
}

When I want to use it on a specific action, I simply tell it that I want to use my custom Json.Net model binder instead:

[HttpPost]
public JsonNetResult MyAction([ModelBinder(typeof(JsonNetModelBinder))] MyModel model)
{
    ...
}

Now my [JsonProperty(PropertyName = "")] attributes are no longer ignored on MyModel and everything is bound correctly!

Tryptophan answered 1/12, 2015 at 21:26 Comment(1)
A version with the dummy checks is available at github.com/hotjk/dot-rbac/blob/master/Grit.Utility.Web/Json/…Studley
M
1

In my case, I had to deserialize complex objects including interfaces and dynamically loaded types etc. so providing a custom value provider does not work as MVC still needs tries to figure out how to instantiate interfaces and then fails.

As my objects were already properly annotated to work with Json.NET, I took a different route: I've implemented a custom model binder and used Json.NET to explicitly deserialize the request body data like this:

internal class CustomModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        // use Json.NET to deserialize the incoming Position
        controllerContext.HttpContext.Request.InputStream.Position = 0; // see: https://mcmap.net/q/501779/-help-reading-json-from-httpcontext-inputstream
        Stream stream = controllerContext.RequestContext.HttpContext.Request.InputStream;
        var readStream = new StreamReader(stream, Encoding.UTF8);
        string json = readStream.ReadToEnd();
        return JsonConvert.DeserializeObject<MyClass>(json, ...);
    }
}

The custom model binder is registered in Global.asax.cs:

  ModelBinders.Binders.Add(typeof(MyClass), new CustomModelBinder();
Malisamalison answered 2/11, 2015 at 12:51 Comment(1)
You could make this more reusable by adding a generic type constraint. CustomModelBinder<T>. Then "return JsonConvert.DeserializeObject<T>(json)". Usage: "ModelBinders.Binders.Add(typeof(MyClass), new CustomModelBinder<MyClass>();"Arbitration

© 2022 - 2024 — McMap. All rights reserved.