Using Json.NET for JSON Model Binding - json

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.

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)

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;
}

Related

ASP.NET MVC DataBinder not deserializing simple types from JSON

Input JSON:
{ "name": "gerry" }
Action method:
{ public ActionResult GenerateQrCode([FromBody] string name }
Problem:
The simple-type args are null
ModelState: Invalid
The built-in json deserializer can't handle the input in this form
I've tried:
ConfigureServices() -> services.AddControllersWithViews().AddNewtonsoftJson(); to switch to NewtonSoft, which I know/love
I've set a break-point into the non-NewtonSoft built-in MS SystemTextJsonInputFormatter.ctor() just to check, if it's still used: yes, it is, I'm not sure why, when I'm calling the above .AddNewtonsoftJson()
The situation:
The client POSTs all the input params as one JSON string document, which is UTF8 w/out BOM
The string comes in at the server-side and is nicely readable with new System.IO.StreamReader(Request.Body).ReadToEnd() from inside the immediate window
I need a way ASP.NET Core deserializes this, as it was able under the .NET4.X for many years w/out any issue
I wouldn't like to add [FromBody] and similar opt-in signatures all over the server actions/args
You pass the name as json but accept as a string so it will be null, you can use an InputFormatter like:
public class RawJsonBodyInputFormatter : InputFormatter
{
public RawJsonBodyInputFormatter()
{
this.SupportedMediaTypes.Add("application/json");
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
var request = context.HttpContext.Request;
using (var reader = new StreamReader(request.Body))
{
var content = await reader.ReadToEndAsync();
return await InputFormatterResult.SuccessAsync(content);
}
}
protected override bool CanReadType(Type type)
{
return type == typeof(string);
}
}
In startup.cs:
services
.AddMvc(options =>
{
options.InputFormatters.Insert(0, new RawJsonBodyInputFormatter());
});
And then you can get the row string
To deserilize it, you can check this, use Newtonsoft and make the string to a Model
[HttpPost]
public IActionResult GenerateQrCode([FromBody] string name)
{
object o = JsonConvert.DeserializeObject(name);
MyModel my = JsonConvert.DeserializeObject<MyModel>(o.ToString());
return View();
}

Exclude a data member from JSon serialization

This is with the Docusign Rest api. When I call ToJson() on an EnvelopeDefinition, it returns the correct info, but I would like it to not serialize the base64 array for when I am writing this out to a log file. I tried using the [JsonIgnore] directive, but that stopped the array from being serialized altogether. Do I need to override the Serialize method on this class or just create another method, something like ToJsonForLogging() and not serialize that array?
I have created an extension method that will work for you. You can call this extension method in your code as follows
string json = envelopeDefinition.ToJsonLog(logDocumentBase64:false)
I am copying the DocumentBase64 into a temporary List and then using .ToJson() function to log without the documentBase64 property.
public static class EnvelopeDefinitionExtensions
{
public static string ToJsonLog(this EnvelopeDefinition envDefinition, bool logDocumentBase64 = true)
{
if (logDocumentBase64) return envDefinition.ToJson();
var tempDocumentBase64List = new List<string>();
foreach(var doc in envDefinition.Documents)
{
tempDocumentBase64List.Add(doc.DocumentBase64);
doc.DocumentBase64 = null;
}
string json = envDefinition.ToJson();
int i =0;
foreach(var doc in envDefinition.Documents)
{
doc.DocumentBase64 = tempDocumentBase64List[i];
i++;
}
return json;
}
}

Web API - Converting JSON to Complex Object Type (DTO) with inheritance

I have the following scenario:
public class WidgetBaseDTO
{
public int WidgetID
{
get;
set;
}
}
public class WidgetTypeA : WidgetBaseDTO
{
public string SomeProperty1
{
get;
set;
}
}
public class WidgetTypeB : WidgetBaseDTO
{
public int SomeProperty2
{
get;
set;
}
}
and my web service returns the following dashboard object whereas the Widgets collection could be of either type A or B:
public class DashboardDTO
{
public List<WidgetBaseDTO> Widgets
{
get;
set;
}
}
my problem is that although the client receives correct JSON content, which is dependent on the Widget type, when reading the response content, they are all being translated to WidgetBaseDTO. what is the correct way to convert these objects to the relevant types?
this is how the response is being read:
string relativeRequestUri = string.Format("api/dashboards/GetDashboard?dashboardID={0}", dashboardID);
using (var client = new HttpClient())
{
// set client options
client.BaseAddress = this.BaseUri;
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// make request
HttpResponseMessage response = client.GetAsync(relativeRequestUri).Result;
if (response.IsSuccessStatusCode)
{
DashboardDTO dashboard = response.Content.ReadAsAsync<DashboardDTO>().Result;
}
I believe after receiving the response you are probably trying to cast WidgetBaseDTO to either WidgetTypeA or WidgetTypeB and you are seeing null? if yes, then you can try after making the following setting to the Json formatter on the server...make sure to make this setting on the client side's json formatter too.
config.Formatters.JsonFormatter.SerializerSettings.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects;
The above setting will cause the type information of WidgetTypeA or WidgetTypeB to be put over the wire which gives a hint to the client as to the actual type of the object being deserialized...you can try looking at the wire format of the response to get an idea...
Client side:
JsonMediaTypeFormatter jsonFormatter = new JsonMediaTypeFormatter();
jsonFormatter.SerializerSettings.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects;
WidgetBaseDTO baseDTO = resp.Content.ReadAsAsync<WidgetBaseDTO>(new MediaTypeFormatter[] { jsonFormatter }).Result;

MVC3 / JSON: How do I use model binding when my property names are renamed via a DataContract?

I use a DataContractJsonSerializer to create a JsonResult for my model data when sending data to the client. My model represents data to be displayed in a data table, and I wished to change the name of the model's properties in the JSON only so that less verbose property names are sent over the wire for each data table row. Now, I'm attempting to send the data table cell values via JSON to the server's controller action method. The names of the fields being sent back are still the short names, and the model binding doesn't seem to like that. What can I do to get model binding working and preserve the ability to sent alternate property names via JSON?
Model:
[DataContract()]
public class UsageListModel {
[DataMember(Name = "results")]
public IEnumerable<UsageModel> Usages { get; set; }
}
[DataContract()]
public class UsageModel {
[DataMember(Name = "job")]
public string JobId { get; set; }
[DataMember(Name = "dt")]
public DateTime UsageDate { get; set; }
[DataMember(Name = "qty")]
public int Quantity { get; set; }
[DataMember(Name = "uom")]
public string UnitOfMeasure { get; set; }
[DataMember(Name = "nts")]
public string Notes { get; set; }
}
It's not as elegant but I usually do this by just making an intermediary class (I refer to it as a ViewModel) that has those shortname properties and can be translated back and forth between it and the actual Model. Although it seems like busy work, the ViewModel can be useful beyond this stint - for example you can use it to easily cache client-side info if the need arises, or serialize/deserialize exactly what's going to/from the client in tests.
I'm still in disbelief that MVC doesn't offer some easier method to bind using custom attributes (or even the .NET data-contract attributes). Given that it doesn't... your best bet is to implement your own IModelBinder. Use reflection to get the DataMember names of the properties, and look for those values in the binding context.
Here's a great reference on model binding: http://msdn.microsoft.com/en-us/magazine/hh781022.aspx
A good general approach to maintaining custom binders: http://lostechies.com/jimmybogard/2009/03/18/a-better-model-binder/
EDIT
Generic model binder that handles a defined type. To add this to your application, add this line in global.asax:
ModelBinders.Binders.Add(typeof(UsageModel), new CustomModelBinder<UsageModel>());
And the binder:
public class CustomModelBinder<T> : IModelBinder
{
public override bool IsMatch(Type t)
{
return t == typeof(T);
}
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
Type t = typeof(T);
var entity = (bindingContext.Model ?? Activator.CreateInstance(t));
// Cycle through the properties and assign values.
foreach (PropertyInfo p in t.GetProperties())
{
string sourceKey;
// this is what you'd do if you wanted to bind to the property name
// string sourceKey = p.Name;
// TODO bind sourceKey to the name in attribute DataMember
Type propertyType = p.PropertyType;
// now try to get the value from the context ...
ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(sourceKey);
if (valueResult != null)
{
bindingContext.ModelState.SetModelValue(sourceKey, valueResult);
p.SetValue(entity, valueResult.ConvertTo(propertyType), null);
}
}
return entity;
}
}
I stumbled across a potential answer to this question randomly while browsing this other question.
I never realized this until now, but apparently you can add attributes to method parameters. Let's take a simple example:
public ActionResult SomeMethod(string val) {
return View(val);
}
If you call this URL -- /MyController/SomeMethod?val=mytestval -- then you'll get back "mytestval" in the model, right? So now you can write this:
public ActionResult SomeMethod([Bind(Prefix="alias")] string val) {
return View(val);
}
Now this URL will produce the same result: /MyController/SomeMethod?alias=mytestval.
Anyway, I'm still not sure if that will answer your question, but I thought it was very interesting.

ASP.Net MVC3 - why does the default support for JSON model binding fail to decode to enum types?

I am having an issue with ASP.Net MVC3 (RC2). I'm finding that the new JSON model binding functionality, which is implicit in MVC3, does not want to deserialize to a property that has an enum type.
Here's a sample class and enum type:
public enum MyEnum { Nothing = 0, SomeValue = 5 }
public class MyClass
{
public MyEnum Value { get; set; }
public string OtherValue { get; set; }
}
Consider the following code, which successfully passes the unit test:
[TestMethod]
public void Test()
{
var jss = new JavaScriptSerializer();
var obj1 = new MyClass { Value = MyEnum.SomeValue };
var json = jss.Serialize(obj1);
var obj2 = jss.Deserialize<MyClass>(json);
Assert.AreEqual(obj1.Value, obj2.Value);
}
If I serialize obj1 above, but then post that data to an MVC3 controller (example below) with a single parameter of type MyClass, any other properties of the object deserialize properly, but any property that is an enum type deserializes to the default (zero) value.
[HttpPost]
public ActionResult TestAction(MyClass data)
{
return Content(data.Value.ToString()); // displays "Nothing"
}
I've downloaded the MVC source code from codeplex but I'm stumped as to where the actual code performing the deserialization occurs, which means I can't work out what the folks at Microsoft have used to perform the deserialization and thus determine if I'm doing something wrong or if there is a workaround.
Any suggestions would be appreciated.
I've found the answer. I hope this is fixed in MVC3 RTM, but essentially what happens is the object deserializes correctly internally via JsonValueProviderFactory, which uses JavaScriptSerializer to do the work. It uses DeserializeObject() so that it can pass the values back to the default model binder. The problem is that the default model binder won't convert/assign an int value when the property type is an enum.
There is a discussion of this at the ASP.Net forums here:
http://forums.asp.net/p/1622895/4180989.aspx
The solution discussed there is to override the default model binder like so:
public class EnumConverterModelBinder : DefaultModelBinder
{
protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
{
var propertyType = propertyDescriptor.PropertyType;
if(propertyType.IsEnum)
{
var providerValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if(null != providerValue)
{
var value = providerValue.RawValue;
if(null != value)
{
var valueType = value.GetType();
if(!valueType.IsEnum)
{
return Enum.ToObject(propertyType, value);
}
}
}
}
return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
}
}
Then in Application_Start, add the following line:
ModelBinders.Binders.DefaultBinder = new EnumConverterModelBinder();
How are you calling this action? Have you tried:
$.post(
'/TestAction',
JSON.stringify({ OtherValue : 'foo', Value: 5 }),
function(result) {
alert('ok');
}
);