dayman
dayman

Reputation: 680

WebApi Fails to Honor JsonObjectAttribute Settings

So I have an ApiController...

public class MetaDataController : ApiController
{
    [HttpPost]
    public HttpResponseMessage Test(TestModel model)
    {
        //Do Stuff
        return  new HttpResponseMessage(HttpStatusCode.OK);
    }
}

That accepts a model...

[JsonObject(ItemRequired = Required.Always)]
public class TestModel
{
    public int Id { get; set; }
    public IEnumerable<SubModel> List { get; set; } 
}

public class SubModel
{
    public int Id { get; set; }
}

In the form of Json...

{  "Id": 1,  "List": [{ "Id": 11 }, { "Id": 12 } ] }

When posting to this controller action, the attribute on TestModel should make Json.Net throw a JsonSerializationException when the Json is missing a property. I wrote unit tests around ensuring this behavior works as expected.

[Test]
public void Test()
{
    var goodJson = @"{ 'Id': 1, 
                        'List': [ {'Id': 11}, {'Id': 12} ]
                        }";

    Assert.DoesNotThrow(() => JsonConvert.DeserializeObject<TestModel>(goodJson));

    var badJson = @"{ 'Id': 1 }";

    Assert.That(()=>JsonConvert.DeserializeObject<TestModel>(badJson),
        Throws.InstanceOf<JsonSerializationException>().
        And.Message.Contains("Required property 'List' not found in JSON."));
}

When posting well-formed Json to the controlelr action, everything works fine. But if that json is missing a required property, no exception is thrown. The members of TestModel that map to the missing properties are null.

Why does JsonConvert work as expected, but the automatic Json deserialization through the WebApiController fail to honor the attributes on TestModel?

Upvotes: 2

Views: 650

Answers (1)

dayman
dayman

Reputation: 680

For giggles, I decided to be EXTRA sure that my application was using Json.Net for json deserialization. So I wrote a MediaTypeFormatter

public class JsonTextFormatter : MediaTypeFormatter
{
    public readonly JsonSerializerSettings JsonSerializerSettings;
    private readonly UTF8Encoding _encoding;

    public JsonTextFormatter(JsonSerializerSettings jsonSerializerSettings = null)
    {
        JsonSerializerSettings = jsonSerializerSettings ?? new JsonSerializerSettings();

        SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
        _encoding = new UTF8Encoding(false, true);
        SupportedEncodings.Add(_encoding);
    }

    public override bool CanReadType(Type type)
    {
        if (type == null)
        {
            throw new ArgumentNullException();
        }

        return true;
    }

    public override bool CanWriteType(Type type)
    {
        return true;
    }

    public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
    {
        var serializer = JsonSerializer.Create(JsonSerializerSettings);

        return Task.Factory.StartNew(() =>
        {
            using (var streamReader = new StreamReader(readStream, _encoding))
            {
                using (var jsonTextReader = new JsonTextReader(streamReader))
                {
                    return serializer.Deserialize(jsonTextReader, type);
                }
            }
        });
    }

    public override Task WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext)
    {
        var serializer = JsonSerializer.Create(JsonSerializerSettings);
        return Task.Factory.StartNew(() =>
        {
            using (
                var jsonTextWriter = new JsonTextWriter(new StreamWriter(writeStream, _encoding))
                {
                    CloseOutput = false
                })
            {
                serializer.Serialize(jsonTextWriter, value);
                jsonTextWriter.Flush();
            }
        });
    }
}

And modified my WebApiConfig to use it instead of the default.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new HandleSerializationErrorAttribute());

        config.Formatters.RemoveAt(0);
        var serializerSettings = new JsonSerializerSettings
        {
            MissingMemberHandling = MissingMemberHandling.Error
        };
        config.Formatters.Insert(0, new JsonTextFormatter(serializerSettings));

    }
}

I also added a ExceptionFilterAttribute to catch serialization errors and return relevant information about what was what was wrong.

public class HandleSerializationErrorAttribute : ExceptionFilterAttribute
{
   public override void OnException(HttpActionExecutedContext context)
    {
        if (context.Exception is JsonSerializationException)
        {
            var responseMessage = new HttpResponseMessage(HttpStatusCode.BadRequest);
            responseMessage.Content = new StringContent(JsonConvert.SerializeObject(context.Exception.Message));

            context.Response = responseMessage;
        }
    }
}

So there it is: .net MVC 4 WebApi SAYS that it uses Json.Net, but the default JsonFormatter refused to obey Json attributes that decorated my models. Explicitly setting the formatter manually fixes the problem.

Upvotes: 1

Related Questions