1

I wonder if anyone else has come across this situation, that when posting JSON the FromBody Attribute disregards JsonProperty PropertyNames's that contain hypens.

I have commented the outputs in my code below to demonstrate what is posted, I can happily deserialize the HttpPost using Request.InputStream, but hoping there is a simpler more efficient method using the FromBody Attribute.

public class Test
{
    [JsonProperty("id")]
    public int Id { get; set; }

    [JsonProperty("forename")]
    public string Forename { get; set; }

    [JsonProperty("address")]
    public Address Address { get; set; }

    [JsonProperty("records-submissions")]
    public IList<RecordsSubmission> RecordsSubmissions { get; set; }

    [JsonProperty("anotherrecordssubmissions")]
    public IList<RecordsSubmission> AnotherRecordsSubmissions { get; set; }
}

public class Address
{
    [JsonProperty("value")]
    public string Value { get; set; }
}

public class RecordsSubmission
{
    [JsonProperty("record-id")]
    public int RecordId { get; set; }

    [JsonProperty("timestamp")]
    public long Timestamp { get; set; }
}

using (var webClient = new WebClient())
{
    var address = new Api.Address {Value = "Somewhere"};

    var recordSubmission = new Api.RecordsSubmission
    {
        RecordId = 2,
        Timestamp = 1497541704606
    };

    var recordsSubmissions = new List<Api.RecordsSubmission> {recordSubmission};

    var anotherRecordsSubmissions = new List<Api.RecordsSubmission> {recordSubmission};

    var test = new Api.Test
    {
        Id = 1,
        Forename = "Joe",
        Address = address,
        RecordsSubmissions = recordsSubmissions,
        AnotherRecordsSubmissions = anotherRecordsSubmissions
    };

    var testSerializeObject = JsonConvert.SerializeObject(test);

    // testSerializeObject Output
    // {"id":1,"forename":"Joe","address":{"value":"Somewhere","versions":null},"records-submissions":[{"record-id":2,"timestamp":1497541704606}],"anotherrecordssubmissions":[{"record-id":2,"timestamp":1497541704606}]}

    var testDeserializeObject = JsonConvert.DeserializeObject<Api.Test>(testSerializeObject);

    // Correct deserialization occurred, class Test is fully populated

    var serializeObject = JsonConvert.SerializeObject(testDeserializeObject);

    // serializeObject Output
    // {"id":1,"forename":"Joe","address":{"value":"Somewhere","versions":null},"records-submissions":[{"record-id":2,"timestamp":1497541704606}],"anotherrecordssubmissions":[{"record-id":2,"timestamp":1497541704606}]}

    webClient.Headers.Add(HttpRequestHeader.ContentType, "application/json");
    webClient.Encoding = Encoding.UTF8;
    webClient.UploadString(new Uri("https://localhost:44380/api-test"), "POST", serializeObject);
}

[System.Web.Mvc.HttpPost]
public ActionResult Test([FromBody] Api.Test test)
{
    // The class test is only partially populated, 
    // [FromBody] ignores JsonProperty PropertyNames with hypens
    // RecordsSubmission with the JsonProperty of "records-submissions" was isgnored and test.RecordsSubmission was null, where as AnotherRecordsSubmissions was fully populated
    try
    {
        var requestInputStream = Request.InputStream;
        requestInputStream.Seek(0, SeekOrigin.Begin);

        var json = new StreamReader(requestInputStream).ReadToEnd();

        var testDeserializeObject = JsonConvert.DeserializeObject<Api.Test>(json);

        // Correct deserialization occurred
    }
    catch (Exception)
    {
        // Do something
    }

    return Redirect("/");
}

Update

Still no further using [FromBody] to deal with hyphens, however using a ModelBinder approach, similar to that found on https://stackoverflow.com/questions/23995210/how-to-use-json-net-for-json-modelbinding-in-an-mvc5-project has provided a workaround, please see my code below to demonstrate:

public class JsonNetModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext modelBindingContext)
    {
        controllerContext.HttpContext.Request.InputStream.Position = 0;

        var inputStream = controllerContext.RequestContext.HttpContext.Request.InputStream;

        var streamReader = new StreamReader(inputStream, Encoding.UTF8);

        var json = streamReader.ReadToEnd();

        return JsonConvert.DeserializeObject(json, modelBindingContext.ModelType);
    }
}

[System.Web.Mvc.HttpPost]
public ActionResult Test([ModelBinder(typeof(JsonNetModelBinder))] Api.Test test)
{
}
adiga
  • 34,372
  • 9
  • 61
  • 83
iggyweb
  • 2,373
  • 12
  • 47
  • 77
  • Can you show us a screenshot (from Postman or your browser dev tools) of the payload being POSTed? – mjwills Jun 19 '17 at 13:46
  • I'm using Visual Studio 2015, any specific object you want me to screenshot? – iggyweb Jun 19 '17 at 13:53
  • Alternatively add this code to your Action and show us the value of `bob`. `Stream req = Request.InputStream; req.Seek(0, System.IO.SeekOrigin.Begin); string bob = new StreamReader(req).ReadToEnd();` – mjwills Jun 19 '17 at 13:56
  • {"id":1,"forename":"Joe","address":{"value":"Somewhere","versions":null},"records-submissions":[{"record-id":2,"timestamp":1497541704606}],"anotherrecordssubmissions":[{"record-id":2,"timestamp":1497541704606}]} – iggyweb Jun 19 '17 at 14:06
  • The data is exactly the same as serializeObject. – iggyweb Jun 19 '17 at 14:08
  • Do you have a model binder defined for `Api.Test`? Another approach you might also try is https://stackoverflow.com/a/38963848/34092 ... – mjwills Jun 19 '17 at 21:58
  • I have this with `var testDeserializeObject = JsonConvert.DeserializeObject(json);` the issue relates to the FromBody attribute as it is ignoring the JsonProperty as there is a hyphen in the PropertyName. – iggyweb Jun 20 '17 at 08:10
  • You are using System.Web.*Http*.FromBody (WebApi) and System.Web.*Mvc*.HttpPost (Mvc). But apart from that, it seems that the default Json formatter of Mvc cannot handle this. You could try to override the formatter, but you can also add WebApi to your project to handle the request. –  Jun 20 '17 at 10:17
  • I'm using System.Net, System.Web.Http, System.Web.Mvc and Newtonsoft.Json, I also have WebApi installed. – iggyweb Jun 20 '17 at 12:05
  • Are you using this method as API call or as response from an MVC view? If it is the first then you can solve this issue by moving the method to an ApiController. –  Jun 20 '17 at 13:07
  • This code is in its own ApiController which is part of an MVC project. – iggyweb Jun 20 '17 at 13:40
  • If I move Test to ApiController then it won't compile: Cannot implicitly convert type 'System.Web.Http.Results.RedirectResult' to 'System.Web.Mvc.ActionResult'. Your controller inherits from ApiController? –  Jun 20 '17 at 16:04
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/147185/discussion-between-iggyweb-and-ruard-van-elburg). – iggyweb Jun 20 '17 at 16:06

1 Answers1

1

You are using var requestInputStream = Request.InputStream; which means that the method is located in a Controller class. Inside an ApiController class this would give a compile error.

It seems that MVC and API are mixed up. FromBody should be used inside an ApiController (uses System.Web.Http). But the method has a System.Web.Mvc.HttpPost attribute (and is located inside a controller).

[System.Web.Mvc.HttpPost]
public ActionResult Test([FromBody] Api.Test test)

In this case it doesn't matter as FromBody can be omitted since there are no other parameters. By default the request is deserialized in the 'test' parameter.

So the problem lies not in FromBody.

The question is: what should the method return? Should it redirect to home or should it respond with StatusCode(HttpStatusCode.Created)?

If you want to redirect then consider to POST a form instead of Json. This does not resolve the hyphen issue, but this is more logical. And you can add an AntiForgery token.

Another option is to use a DTO without the hyphens or deserialize Json from the InputStream, like you do in the code from your update.

So you can use the controller class for calls like this, but you cannot resolve the issue with the hypens unless you do something to handle the json yourself.

You can resolve the hyphen problem quite easy by moving the method to an ApiControler class. This is sufficient to solve the hyphen issue.

public class SomeApiController : ApiController
{
    [HttpPost]
    public IHttpActionResult Test(Api.Test test)
    {
        var res = test.RecordsSubmissions;
        return StatusCode(HttpStatusCode.Created);
    }

    // Is equivalent of Test:
    [HttpPost]
    public void Test2(Api.Test test)
    {
        var res = test.RecordsSubmissions;

    }
}
  • 1
    The project was initially MVC5, I added WebApi via Nuget to the project, created a new Controller and as you have rightly spotted, its a default Controller not an ApiController. Since then, I have followed the solution provided by lko `https://stackoverflow.com/questions/26067296/how-to-add-web-api-to-an-existing-asp-net-mvc-5-web-application-project` to correctly add WebApi to an existing MVC5 project, then by adding a new Controller with type System.Web.Http.ApiController I was able to reintroduce the [FromBody] attribute and it works perfectly with hyphens, many thanks :-) – iggyweb Jun 21 '17 at 13:32