10
public class MyModel
{
    [JsonProperty(PropertyName = "foo", Required = Required.Always)]
    public String Bar;
}

public class MyController : ApiController
{
    public String PostVersion1([FromBody] MyModel myModel)
    {
        if (ModelState.IsValid)
        {
            if (myModel.Bar == null)
                return "What?!";
            else
                return "Input is valid.";
        }
        else
        {
            return "Input is invalid.";
        }
    }
}

Results:

Input              |Output
-------------------|------
{ "bad" : "test" } | What?!
{ "Bar" : "test" } | What?!
{ "foo" : "test" } | Input is valid.

JsonPropertyAttribute is clearly supported because I am able to set the PropertyName and have it take effect. However, I would expect the ModelState.IsValid to be false for the first two example inputs because the Required JsonProprty parameter was set to Always.

If I just run it through JsonConvert:

JsonConvert.DeserializeObject<MyModel>(@"{'bad':'test'}");

an exception is thrown during deserialization as expected:

Result Message: Newtonsoft.Json.JsonSerializationException : Required property 'foo' not found in JSON. Path '', line 1, position 14.
Micah Zoltu
  • 6,764
  • 5
  • 44
  • 72

4 Answers4

5

The default JsonMediaTypeFormatter does not rely on on he JsonProperty to decide whether model fields are required or not. It does rely however on the RequiredAttribute

If you want to do this then implement a new IRequiredMemberSelector and set it to MediaTypeFormatter.RequiredMemberSelector.

In your implementation of IRequiredMemberSelector you will be passed a MemberInfo. You can use that to evaluate if model members have the JsonProperty attribute and if the required flag is set, and finally return true or false. This will be propagated to the ModelState.IsValid property (it will not use the JSON.NET error message though, but the DataAnnotations/WebApi one.

If you do this, then I suggest you also keep the default behavior.

Marcel N.
  • 13,726
  • 5
  • 47
  • 72
  • I can't use RequiredAttribute because it appears that that takes effect before the name routing of the JSON deserializer. In the above examples, { "Bar" : "test" } will validate even though once it deserializes Bar will be null. – Micah Zoltu Aug 02 '14 at 06:36
  • It would seem that the RequiredMemberSelector is run first, and then after it is validated the type is deserialized. Is it possible to deserialize and then validate or do both in one step without having to litter my controller actions with JsonConvert.Deserialize calls? It seems that the more correct course of action here (especially since I want to return an error on deserialization failure) would be to deserialize first and then call the action if deserialization was a success. If I am understanding correctly, validation occurs first then deserialization. – Micah Zoltu Aug 02 '14 at 06:39
  • @MicahCaldwell: Let me check. – Marcel N. Aug 02 '14 at 06:46
  • @MicahCaldwell: Actually, if you deserialize first then validate you lose the JSON and therefore any meaningful error. You won't be able to output an error like `JsonConvert` does. So, it rather should be the other way around. Have a look also at `JsonContractResolver.ConfigureProperty`. It always sets `property.Required = Required.AllowNull`, so that needs to be changed as well. This happens prior to deserialization. – Marcel N. Aug 02 '14 at 06:59
  • 1
    @MicahCaldwell: Not sure anymore if it will work at all using this path. If you take a look at `BaseJsonMediaTypeFormatter.ReadFromStream` it becomes obvious that any JSON.NET exception is silenced. – Marcel N. Aug 02 '14 at 07:17
3

In order to solve this I ended up creating my own custom JSON.NET MediaTypeFormatter. My formatter allows the JSON.NET deserialization exceptions bubble out which results in the exception information being returned to the caller.

Here is the MediaTypeFormatter I built:

public class JsonMediaFormatter : MediaTypeFormatter
{
    private readonly JsonSerializer _jsonSerializer = new JsonSerializer();

    public JsonMediaFormatter()
    {
        SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
    }

    public override Boolean CanReadType(Type type)
    {
        if (type == null)
            return false;

        return true;
    }

    public override Boolean CanWriteType(Type type)
    {
        if (type == null)
            return false;

        return true;
    }

    public override Task<Object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
    {
        return Task.FromResult(Deserialize(readStream, type));
    }

    public override Task WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext, CancellationToken cancellationToken)
    {
        Serialize(writeStream, value);
        return Task.FromResult(0);
    }

    private Object Deserialize(Stream readStream, Type type)
    {
        var streamReader = new StreamReader(readStream);
        return _jsonSerializer.Deserialize(streamReader, type);
    }

    private void Serialize(Stream writeStream, Object value)
    {
        var streamWriter = new StreamWriter(writeStream);
        _jsonSerializer.Serialize(streamWriter, value);
        streamWriter.Flush();
    }
}

In order to use this formatter over the built-in one, I added this line to my WebApiConfig:

config.Formatters.Insert(0, new Formatters.JsonMediaFormatter());

By inserting it at index 0, it takes precedence over the built-in formatter. If you care, you could remove the built-in JSON formatter.

In this scenario, the ModelState is always valid in the action because an exception is thrown back to the user before the action is ever fired if deserialization fails. More work would need to be done in order to still execute the action with a null FromBody parameter.

Micah Zoltu
  • 6,764
  • 5
  • 44
  • 72
3

I know this is an old question but I solved it like this:

var formatter = new JsonMediaTypeFormatter {
    SerializerSettings = {
        ContractResolver = new DefaultContractResolver(true)
    }
};
configuration.Formatters.Insert(0, formatter);

The parsing errors will then be included in ModelState

adrianm
  • 14,468
  • 5
  • 55
  • 102
1

If you only want to support JSON, then you can do this:

public String PostVersion1([FromBody] JObject json)
{
  if(json == null) {
    // Invalid JSON or wrong Content-Type
    throw new HttpResponseException(HttpStatusCode.BadRequest);
  }

  MyModel model;
  try
  {
    model = json.ToObject<MyModel>();
  }
  catch(JsonSerializationException e)
  {
    // Serialization failed
    throw new HttpResponseException(HttpStatusCode.BadRequest);
  }
}

But you don't want to do this in every request handler. This is one of multiple flaws of the [FromBody] attribute. See here for more details: https://stackoverflow.com/a/52877955/2279059

Florian Winter
  • 4,750
  • 1
  • 44
  • 69