3

In asp.net core 2.1, when a controller action is set as:

    [HttpPost]
    public JsonResult GetAnswer(SampleModel question)
    {               
        return Json(question.Answer);
    }

where the SampleModel is defined as:

public class SampleModel
{
    [Required]
    public string Question { get; set; }

    public string Answer { get; set; }
}

this is still considered as valid request:

{
  "question": "some question",
  "question": "some question 2",
  "answer": "some answer"
}

In the controller I can see that second question is the value of model and model is valid.

Question is how to validate just body of request as valid JSON even before model binding?

Zaak
  • 482
  • 11
  • 25
  • Try catch, and test of it the object you expect, if not, it not a valid model – JohnnBlade Mar 14 '19 at 03:38
  • if you want the first data, then Request["SampleModel.answer"] or how you called it, but you can also create a special handler for that, but for testing you Request["nameofyrinput"] – JohnnBlade Mar 14 '19 at 03:40
  • @JohnnBlade how would one test if object is not the same as I expect if deserialization succeeds? – Zaak Mar 14 '19 at 19:00

1 Answers1

4

According to Timothy Shields's answer, it's hard to say that would be an invalid json if we have duplicated property keys.

And when using ASP.NET Core 2.1, it won't throw at all.

As of 12.0.1, the Newtonsoft.Json has a DuplicatePropertyNameHandling settings. It will throw if we set DuplicatePropertyNameHandling.Error and pass a duplicated property. So the easiest way I can come up is to create a custom model binder. We could deserialize the JSON and change the ModelState if it throws .

Fristly, install the latest Newtonsoft.Json:

  <ItemGroup>
    <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
  </ItemGroup>

and then register a JsonLoadSettings option as a singleton service for later reuse :

services.AddSingleton<JsonLoadSettings>(sp =>{
    return new JsonLoadSettings { 
        DuplicatePropertyNameHandling =  DuplicatePropertyNameHandling.Error,
    };
});

Now we can create a custom model binder to deal with duplicated properties :

public class XJsonModelBinder: IModelBinder
{
    private JsonLoadSettings _loadSettings;
    public XJsonModelBinder(JsonLoadSettings loadSettings)
    {
        this._loadSettings = loadSettings;
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); }
        var modelName = bindingContext.BinderModelName?? "XJson";
        var modelType = bindingContext.ModelType;

        // create a JsonTextReader
        var req = bindingContext.HttpContext.Request;
        var raw= req.Body;
        if(raw == null){ 
            bindingContext.ModelState.AddModelError(modelName,"invalid request body stream");
            return Task.CompletedTask;
        }
        JsonTextReader reader = new JsonTextReader(new StreamReader(raw));

        // binding 
        try{
            var json= (JObject) JToken.Load(reader,this._loadSettings);
            var o  = json.ToObject(modelType);
            bindingContext.Result = ModelBindingResult.Success(o);
        }catch(Exception e){
            bindingContext.ModelState.AddModelError(modelName,e.ToString()); // you might want to custom the error info
            bindingContext.Result = ModelBindingResult.Failed();
        }
        return Task.CompletedTask;
    }
}

To enable read the Request.Body multiple times, we could also create a dummy Filter:

public class EnableRewindResourceFilterAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        context.HttpContext.Request.EnableRewind();
    }
    public void OnResourceExecuted(ResourceExecutedContext context) { }
}

Lastly, decorate the action method with [ModelBinder(typeof(XJsonModelBinder))] and EnableRewindResourceFilter:

    [HttpPost]
    [EnableRewindResourceFilter]
    public JsonResult GetAnswer([ModelBinder(typeof(XJsonModelBinder))]SampleModel question)
    {               
        if(ModelState.IsValid){
            return Json(question.Answer);
        }
        else{
            // ... deal with invalid state
        }
    }

Demo :

enter image description here

itminus
  • 23,772
  • 2
  • 53
  • 88
  • thank you for the answer, somehow it surprised me a bit that as duplicate keys on same level do not technically constitute invalid json, but then again my background is mostly in strongly typed languages, so that might be part of my thinking by now. – Zaak Mar 14 '19 at 18:58