2

I have a method being posted to via AJAX with the following header:

public JsonResult GetDocuments(string searchTerm, SortRequest sort)

The SortRequest object is defined as follows:

[DataContract]
public class SortRequest
{
    [DataMember(Name = "field")]
    public string Field { get; set; }

    [DataMember(Name = "dir")]
    public string Direction { get; set; }
}

Because of legacy code, the JSON object has the property name "dir" which doesn't directly match the C# property name. We want to use Json.NET as the model binder for JSON requests because it is able to handle this, but the problem is that the JSON coming into the model binder looks like a single object with two top level properties, "searchTerm" and "sort". The deserialization process then tries to map that entire JSON string into each method parameter which obviously fails.

I have tried looking through the now open source .NET MVC code and have not yet been able to determine how the DefaultModelBinder class handles this gracefully. The only option I can see so far is to convert every JSON action to take in a single request parameter but this doesn't seem like a good solution as the DefaultModelBinder doesn't require this.

Edit for clarification:

The JSON request string looks something like this:

{
    "searchTerm": "test",
    "sort": {
        "field": "name",
        "dir": "asc"
    }
}

We are overriding the DefaultModelBinder and only using Json.NET when the request is of type application/json. Here is the relevant code:

var request = controllerContext.HttpContext.Request;

request.InputStream.Seek(0, SeekOrigin.Begin);

using (var reader = new StreamReader(request.InputStream))
{
    var jsonString = reader.ReadToEnd();

    result = JsonConvert.DeserializeObject(jsonString, bindingContext.ModelType);
}

The bindingContext.ModelType is going to be set to String and SortRequest for each parameter in the method, but since the above is a single JSON object, it doesn't map to either of those types and thus inside the method itself, everything is set to default values.

1
  • Look for [JsonProperty()], and use it in your Model, for every property. Commented May 18, 2015 at 14:18

2 Answers 2

1

I think the JsonProperty attribute can be used for this as follows:

[DataContract]
public class SortRequest
{
    [DataMember(Name = "field")]
    [JsonProperty("field")]
    public string Field { get; set; }

    [DataMember(Name = "dir")]
    [JsonProperty("dir")]
    public string Direction { get; set; }
}

Update

Based upon the json add a binding prefix:

public JsonResult GetDocuments(string searchTerm, [Bind(Prefix="sort"] SortRequest sort)
Sign up to request clarification or add additional context in comments.

5 Comments

I've added some more clarification in the original question. The DataMember attribute seems to be respected by Json.NET when it is the only method parameter. The problem is that when there are multiple parameters trying to be mapped to via a single JSON object.
No dice, the entire request string is still trying to be deserialized. Your suggested did get me thinking that maybe our model binder code itself might be the issue. I've updated above with the relevant part of the BindModel method. I'm thinking maybe we are getting the parameter itself in the wrong way.
@rpf3 Sorry, I'm not sure I think that usually works for me. I will delete so hopefully someone else can answer.
No worries, don't delete either. I think it's useful to have all options here so people are aware what has been tried. Thanks
@rpf3 Ok, no probs. Will leave it for now. Good luck. :)
1

I ended up going with a solution using the JToken.Parse method in the Json.NET library. Essentially what is happening is that we check the top level properties of the JSON object and see if there exists the current action parameter we are trying to bind to. Where this falls down is if there is overlap between the parameter name of the action and a property name of a single request being passed in. I think this is enough of an edge case to let slide as it would require only a single object be passed into an action that is expecting multiple.

Here is the modified BindModel method:

public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
    object result;

    if (IsJSONRequest(controllerContext))
    {
        var request = controllerContext.HttpContext.Request;

        request.InputStream.Seek(0, SeekOrigin.Begin);

        using (var reader = new StreamReader(request.InputStream))
        {
            var jsonString = reader.ReadToEnd();

            // Only parse non-empty requests.
            if (!String.IsNullOrWhiteSpace(jsonString))
            {
                // Parse the JSON into a generic key/value pair object.
                var obj = JToken.Parse(jsonString);

                // If the string parsed and there is a top level property of the same
                // name as the parameter name we are looking for, use that property
                // as the JSON object to de-serialize.
                if (obj != null && obj.HasValues && obj[bindingContext.ModelName] != null)
                {
                    jsonString = obj[bindingContext.ModelName].ToString();
                }
            }

            result = JsonConvert.DeserializeObject(jsonString, bindingContext.ModelType);
        }
    }
    else
    {
        result = base.BindModel(controllerContext, bindingContext);
    }

    return result;
}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.