nodatime / nodatime.serialization

Serialization projects for Noda Time

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

NodaTime Serialization ignores Required Attribute

schmitch opened this issue · comments

Actually, when NodaTime has a required Attribute, but the value that the server sended, was null it will fail with the following exception:

NodaTime.Utility.InvalidNodaDataException: Cannot convert null value to NodaTime.LocalDate
   at NodaTime.Serialization.JsonNet.Preconditions.CheckData[T](Boolean expression, String messageFormat, T messageArg)
   at NodaTime.Serialization.JsonNet.NodaConverterBase`1.ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.DeserializeConvertable(JsonConverter converter, JsonReader reader, Type objectType, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.SetPropertyValue(JsonProperty property, JsonConverter propertyConverter, JsonContainerContract containerContract, JsonProperty containerProperty, JsonReader reader, Object target)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Mvc.Formatters.NewtonsoftJsonInputFormatter.ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
   at Microsoft.AspNetCore.Mvc.ModelBinding.Binders.BodyModelBinder.BindModelAsync(ModelBindingContext bindingContext)
   at Microsoft.AspNetCore.Mvc.ModelBinding.ParameterBinder.BindModelAsync(ActionContext actionContext, IModelBinder modelBinder, IValueProvider valueProvider, ParameterDescriptor parameter, ModelMetadata metadata, Object value)
   at Microsoft.AspNetCore.Mvc.Controllers.ControllerBinderDelegateProvider.<>c__DisplayClass0_0.<<CreateBinderDelegate>g__Bind|0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Asvg.SalesTool.Web.Startup.<>c.<<Configure>b__0_2>d.MoveNext() in /Users/schmitch/projects/envisia/asvg/salestool/dotnet-src/Asvg.SalesTool.Web/Startup.Configure.cs:line 99
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Asvg.SalesTool.Web.Startup.<>c.<<Configure>b__0_1>d.MoveNext() in /Users/schmitch/projects/envisia/asvg/salestool/dotnet-src/Asvg.SalesTool.Web/Startup.Configure.cs:line 56
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Model:

public class Model {
        [Required]
        public LocalDate Start { get; set; }
}

the problem here is, that it does not return a correctly formatted error message that LocalDate is null. No instead it fails with an Exception. Which is really really bad.
A temporary fix is to make LocalDate nullable, but that will actually make my type wrong since the actually value is never null. (since the required attribute, will correctly fail).

The problem is actually here: https://github.com/nodatime/nodatime.serialization/blob/master/src/NodaTime.Serialization.JsonNet/NodaConverterBase.cs#L58
the problem is, is that CheckData does not throw an JsonSerializationException, and since it's not in the try block it will fail. instead of raising a JsonSerializationException which can than be correctly used by Json.Net.

How would you expect a "correctly formatted error message" to be returned other than through an exception? It feels to me like an exception is the right result here. Which exception is up for grabs, but the idea that failing with an exception is "really really bad" is one I strenuously disagree with.

The Required aspect feels like it's all on Json.NET to be honest - the Noda Time converter doesn't have access to the information that the Required attribute was applied to the property, so how could it behave differently in that case? It's not clear to me how the Required attribute is relevant at all here - whether the value is required or not, if the value is a JSON null, the conversion will fail.

I can see how using JsonSerializationException could potentially be better here. It would be nice if there were detailed documentation in Json.NET to explain when each type of exception should be used. If you're aware of any such documentation, I'd be happy to try to follow it.

I'm not sure Required is relevant here either: if the server isn't sending a value at all, then I'm surprised that the Noda Time converter would be invoked at all: I would have expected that Json.NET fail the deserialization much earlier if a required property isn't available, rather than pass null to the converter.

On the other hand, if the server is sending an explicit JSON null, then a conversion exception is exactly what I would expect. As Jon says, it may be that our converter should use a different exception type in order to get Json.NET to return a better error message, but that doesn't appear to be documented anywhere.

actually the error was different between either older versions of nodatime or json.net or dotnet core. before it would not raise an exception and instead show the exception message as an error message in json (when combined with ApiControllerAttribute) unfortunatly somewhere in between it changed.
Json.NET actually serializes JsonSerializationException to valid Json so that it does not look as an exception.

However I think the problem is more in the dotnet framework that Required might be honored later instead at the beginning and thus it fails.

Json.NET actually serializes JsonSerializationException to valid Json so that it does not look as an exception.

That sounds like a terrible idea to me - a recipe for weird behavior when things are misconfigured.

However I think the problem is more in the dotnet framework that Required might be honored later instead at the beginning and thus it fails.

Right. Again, that's not something we can do about in NodaTime.Serialization.JsonNet.

So is this issue now just a feature request for "use JsonSerializationException everywhere"?

actually this is a bug, since in .net core 2.2 or older json.net the behavior was different and the model:

public class Model {
        [Required]
        public LocalDate Start { get; set; }
}

never ever raised an Exception. However something changed.

Also: https://docs.microsoft.com/de-de/aspnet/core/mvc/models/validation?view=aspnetcore-3.0#required-attribute
Which says that something like NodaTime (which uses value types) should handle reader.TokenType == JsonToken.Null the same as a decimal which would also not raise an exception and instead serialize a json error message with the value: The field is required..

actually this is a bug

That doesn't mean it's a bug in NodaTime.Serialization.JsonNet though.

Which says that something like NodaTime (which uses value types) should handle reader.TokenType == JsonToken.Null the same as a decimal

I don't see how ASP.NET Core can make a requirement on Json.NET converters. Could you clarify where in that page you're taking that information from? I can't find anything like that, at least in the English version.

Closing due to lack of further information. I'm pretty sure this isn't a problem in this library.