Coder Social home page Coder Social logo

nodatime.serialization's Introduction

nodatime.serialization's People

Contributors

0xced avatar buvinghausen avatar dgarciarubio avatar dsilence avatar hangy avatar jskeet avatar malcolmr avatar petriashev avatar yopcix avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

nodatime.serialization's Issues

Security issue NodaTime.Serialization.SystemTextJson

Hello,

We use [email protected] in some of our projects. We used Snyk to analyse our dependencies and to find vulnerabilities. Snyk reported a vulnerability in one of the dependencies of [email protected]. The dependency [email protected] (used by [email protected], which is in turn used by [email protected]) introduces the following vulnerability: https://www.cve.org/CVERecord?id=CVE-2021-26701

It is my understanding that this vulnerability has been fixed in the upstream packages. Looking at the file NodaTime.Serialization.SystemTextJson.csproj in the main branch (https://github.com/nodatime/nodatime.serialization/blob/a4a0333f6f723c7757c4e114a95d9debfba54f49/src/NodaTime.Serialization.SystemTextJson/NodaTime.Serialization.SystemTextJson.csproj), it seems that the dependency is only defined explicitly for netstandard2.0. For netcoreapp3.1, it is not (so the runtime version will be used I think). I believe this would fix the vulnerability for our projects (we use net6.0) and other projects that target netcoreapp3.1 and further.

However, there has been no release of the NuGet package based on this version of NodaTime.Serialization.SystemTextJson.csproj yet. The latest release uses this version: https://github.com/nodatime/nodatime.serialization/blob/43299d06f284f064f083bdf91ccbf4d95903df9f/src/NodaTime.Serialization.SystemTextJson/NodaTime.Serialization.SystemTextJson.csproj. In this version, the dependency is always defined explicitly.

If my understanding is correct, releasing an up-to-date NuGet package should fix the vulnerability for some contexts. If that is indeed the case, would it be possible for you to do it?

Thank you in advance.

Work out why InheritDoc is failing in Docker

It works locally... but in Docker we have things like this:

obj/Release/netcoreapp3.1/NodaTime.Serialization.SystemTextJson.xml(38,10): InheritDocTask warning IDT002: No matching documentation could be found for: M:NodaTime.Serialization.SystemTextJson.DelegatingConverterBase1.Write(System.Text.Json.Utf8JsonWriter,0,System.Text.Json.JsonSerializerOptions), which attempts to inherit from: M:System.Text.Json.Serialization.JsonConverter1.Write(System.Text.Json.Utf8JsonWriter,0,System.Text.Json.JsonSerializerOptions) [/source/src/NodaTime.Serialization.SystemTextJson/NodaTime.Serialization.SystemTextJson.csproj::TargetFramework=netcoreapp3.1]

Handling clients without lenient parsers

Hi,

For Instant and Duration, this library uses an extended iso pattern with optional milliseconds and microseconds. This is flexible, but a lot of other languages don't have lenient parsers out-of-the-box. (Even JodaTime defaults to a strict format JodaOrg/joda-time#201)

It might be nice for a way to specify a strict format (aka always ss.SSS, for example). Is this something you would consider for inclusion in this library, or should I just write my own JsonConverters?

I can't think of a clean API that would support this, so maybe that's an indication I should just roll my own.

ConfigureForNodaTime(IDateTimeZoneProvider, FormatOptions)?

Michael

Will deserializing into an Instant fail if the UTC indicator (z) isn't present in the string?

I'm probably just to lazy to set up and test this myself, so I thought I might ask if you guys know this off the top of your head.

If I try to deserialize this piece of JSON

{
  "myDate": "2019-07-10T21:27:00"
}

into this type

class MyClass
{
  public Instant MyDate { get; set; }
}

...will the JSON.NET extension accept the value or will it complain about the format not indicating that it's in UTC?

Thanks

System.Text.Json should expose a JsonConverterFactory for defaults

The attribute present in the source does not work with System.Text.Json source generation, and its internal logic is easily translated to also implement a JsonConverterFactory which can more easily be used with the base JsonConverter attribute and therefore support source generation

"JsonException: The JSON value could not be converted to NodaTime.Instant" NodaTime issue with ASP.NET Core 3.1 Razor Page web application

In ASP.NET Core 3.1 Razor Page pure front end web application I received the below error.

Installed the following packages:

<PackageReference Include="System.Text.Json" Version="4.7.2" />
<PackageReference Include="EnumExtensions.System.Text.Json" Version="1.0.0" />
<PackageReference Include="NodaTime" Version="3.0.1" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.0.0" />

Also set this in Startup:

services.AddRazorPages()
  .AddJsonOptions(options =>
  {
      // options.JsonSerializerOptions.PropertyNamingPolicy = null;
      // options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
      // options.JsonSerializerOptions.DictionaryKeyPolicy = null;

      options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverterWithAttributeSupport(null, true, true, true, true));
      //options.JsonSerializerOptions.IgnoreNullValues = true;
      options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
      options.JsonSerializerOptions.Converters.Add(NodaConverters.IntervalConverter);
      options.JsonSerializerOptions.Converters.Add(NodaConverters.InstantConverter);
  })
  .AddRazorPagesOptions(options =>
  {
      options.Conventions.AddPageRoute("/Login", "");
  });

JsonException: The JSON value could not be converted to NodaTime.Instant. Path: $.data[0].created_at | LineNumber: 0 | BytePositionInLine: 261.
System.Text.Json.ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(Type propertyType)
System.Text.Json.JsonPropertyInfoNotNullable<TClass, TDeclaredProperty, TRuntimeProperty, TConverter>.OnRead(ref ReadStack state, ref Utf8JsonReader reader)
System.Text.Json.JsonPropertyInfo.Read(JsonTokenType tokenType, ref ReadStack state, ref Utf8JsonReader reader)
System.Text.Json.JsonSerializer.ReadCore(JsonSerializerOptions options, ref Utf8JsonReader reader, ref ReadStack readStack)
System.Text.Json.JsonSerializer.ReadCore(Type returnType, JsonSerializerOptions options, ref Utf8JsonReader reader)
System.Text.Json.JsonSerializer.Deserialize(string json, Type returnType, JsonSerializerOptions options)
System.Text.Json.JsonSerializer.Deserialize(string json, JsonSerializerOptions options)

Here's the snippet of data it's trying to deserialize. If I switch from Instant to DateTimeOffset, it works "instantly" (pun intended :D)

{
    "data": [
        {
            "created_at": "2020-08-09T22:10:26.274672Z",
            "updated_at": "2020-08-13T02:22:02.640871Z",
        }
    ],
    "page": 1,
    "size": 20,
    "count": 1,
    "total": 1,
    "success": true,
    "message": null
}

Note: this json data is a result of serialization of an object that does include CreatedAt & UpdatedAt properties of the type (NodaTime)Instant. I confirm it works nicely with an asp.net core 3.1 mvc api application.

Not sure why it's not working.

Stack Overflow reference:
https://stackoverflow.com/questions/64356378/jsonexception-the-json-value-could-not-be-converted-to-nodatime-instant-nodat

Different formats used in TypeConverters and serialization for Durations

I love the new type converters in NodaTime 3.0, but the converter for Duration is giving me a headache.

There isn't an ISO 8601 format for Duration (the duration mention there is a Period), but I would have preferred that Noda Time at least used a consistent format.

Noda Time's DurationTypeConverter uses DurationPattern.Roundtrip with the format -D:hh:mm:ss.FFFFFFFFF, whereas NodaTime.Serialization's ConfigureForNodaTime() uses (a custom format equal to) DurationPattern.JsonRoundtrip with the format -H:mm:ss.FFFFFFFFF instead.

That means that in order to have a consistent format everywhere, we need to write code like this:

serializerSettings
    // Adds default serializers
    .ConfigureForNodaTime(DateTimeZoneProviders.Tzdb)

    // Change Duration format to match TypeConverter
    .Converters.Insert(0, new NodaPatternConverter<Duration>(DurationPattern.Roundtrip));

I personally prefer the standard round trip format with days, as it is much easier to understand by a human. But regardless of which format is chosen, I think the two formats should agree.

Add attributes that apply the "standard" JsonConverters

System.Text.Json provides two ways to specify the JsonConverter used to convert a type:

  1. Add the converter to the JsonSerializerOptions.Converters collection
  2. Add a JsonConverterAttribute to the property to be serialized

Option 1 works great, but it requires constructing or passing around a properly-configured JsonSerializerOptions. In our apps, there is no situation where I want an Instant property to be serialized using anything except NodaConverters.InstantConverter.

It would be convenient to provide a JsonConverterAttribute that can be used instead of configuring a JsonSerializerOptions. However, NodaTime.Serialization.SystemTextJson can't do this out of the box because the converters are static properties on NodaConverters, and so can't be passed to the JsonConverterAttribute constructor (which takes a Type).

One option is a single attribute that can be applied to almost any NodaTime type:

public class NodaTimeDefaultJsonConverterAttribute : JsonConverterAttribute
{
    public override JsonConverter? CreateConverter(Type typeToConvert)
    {
        if (typeToConvert == typeof(Instant))
        {
            return NodaConverters.InstantConverter;
        }
        else if (typeToConvert == typeof(Interval))
        {
            return NodaConverters.IntervalConverter;
        }
        // ...
        else
        {
            return null;
        }
    }
}

// Usage:
public class A
{
    [NodaTimeDefaultJsonConverter]
    public Instant CreatedAt { get; set; }

    [NodaTimeDefaultJsonConverter]
    public LocalDate Birthday { get; set; }
}

This couldn't be applied to properties of type DateTimeZone or ZonedDateTime, since a IDateTimeZoneProvider needs to be specified. To avoid making a special case for those two types, a second option would be to provide a separate JsonConverterAttribute for each NodaTime type:

public class InstantConverterAttribute : JsonConverterAttribute
{
    JsonConverter? _converter;

    public InstantConverterAttribute() =>
        _converter = NodaConverters.InstantConverter;

    public override JsonConverter? CreateConverter(Type typeToConvert)
        => _converter;
}

public class TzdbDateTimeZoneConverterAttribute : JsonConverterAttribute
{
    JsonConverter? _converter;

    public TzdbDateTimeZoneConverterAttribute() =>
        _converter = NodaConverters.CreateDateTimeZoneConverter(DateTimeZoneProviders.Tzdb);

    public override JsonConverter? CreateConverter(Type typeToConvert)
        => _converter;
}

// Usage:
public class B
{
    [InstantConverter]
    public Instant CreatedAt { get; set; }

    [LocalDateConverter]
    public LocalDate Birthday { get; set; }

    [TzdbDateTimeZoneConverter]
    public DateTimeZone DisplayTimeZone { get; set; }
}

The converters could be written to allow custom patterns, if desired:

[InstantConverter("uuuu-MM-dd HH:mm:ss")] // would construct a NodaPatternConverter<Instant> on first use

Extension: Please add simplified time format for Time component converters

It is often needed to specify time or date-time using only "HH:mm" as a time component. Many controls allow specifying only hours and minutes. Seconds are excessive for most of the cases.

It would be nice if you could extend the list of acceptable time formats for "HH:mm" for LocalTime and/or " T HH:MM" for LocalDateTime

of course, the string could always be concatenated with ":00" as seconds, but this look as a "hack" in many scenarios.

Thanks

Can't Round Trip Instant using default serializers

Here is a simple repro

[Test]
public void Deserialize_ToNonNullableType_InClass()
{
    var instantInAClass = new InstantTestClass() {Now = SystemClock.Instance.GetCurrentInstant()};
    var json = JsonConvert.SerializeObject(instantInAClass);
    var deserializedInstantInAClass = JsonConvert.DeserializeObject<InstantTestClass>(json, settings);
    var expectedInstant = instantInAClass.Now;
    Assert.AreEqual(expectedInstant, deserializedInstantInAClass.Now);
}

private class InstantTestClass
{
    public Instant Now { get; set; }
}

Expected output is the Assert.AreEqual succeeding. Actual output is an exception ๐Ÿ˜ข

NodaTime.Utility.InvalidNodaDataException : Unexpected token parsing Instant. Expected String, got StartObject.
   at NodaTime.Serialization.JsonNet.NodaPatternConverter`1.ReadJsonImpl(JsonReader reader, JsonSerializer serializer)
   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)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
   at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings)
   at NodaTime.Serialization.Test.JsonNet.NodaInstantConverterTest.Deserialize_ToNonNullableType_InClass()

Add Protobuf support for NodaTime.DateTimeZone

If I understand http://protobuf-net.github.io/protobuf-net/nodatime.html correctly, the type NodaTime.DateTimeZone is not supported. It would be great if that support could be added.

I have already asked Marc Gravell in the issue under protobuf-net/protobuf-net#787 and shipped the feature request now to here.

I really would appreciate a short workaround via a surrogate or something as a short term fix if this is a more complex change.
Something like I did for DateTimeOffset maybe:

Class Startup.cs:

RuntimeTypeModel.Default.Add(typeof(DateTimeOffset), false).SetSurrogate(typeof(DateTimeOffsetSurrogate));
RuntimeTypeModel.Default.Add(typeof(DateTimeOffset?), false).SetSurrogate(typeof(DateTimeOffsetSurrogate));

Class DateTimeOffsetSurrogate.cs:

using System;

using ProtoBuf;

[ProtoContract(Name = nameof(DateTimeOffset))]
public class DateTimeOffsetSurrogate
{
    [ProtoMember(1)]
    public long? Value { get; set; }

    public static implicit operator DateTimeOffset(DateTimeOffsetSurrogate surrogate)
    {
        if (surrogate?.Value is null)
        {
            throw new ArgumentNullException(nameof(surrogate));
        }

        var dateTime = DateTimeOffset.FromUnixTimeMilliseconds(surrogate.Value.Value);
        dateTime = dateTime.ToLocalTime();
        return dateTime;
    }

    public static implicit operator DateTimeOffset?(DateTimeOffsetSurrogate? surrogate)
    {
        if (surrogate?.Value == null)
        {
            return null;
        }

        var dateTime = DateTimeOffset.FromUnixTimeMilliseconds(surrogate.Value.Value);
        dateTime = dateTime.ToLocalTime();
        return dateTime;
    }

    public static implicit operator DateTimeOffsetSurrogate(DateTimeOffset source)
    {
        return new ()
        {
            Value = source.ToUnixTimeMilliseconds()
        };
    }

    public static implicit operator DateTimeOffsetSurrogate(DateTimeOffset? source)
    {
        return new ()
        {
            Value = source?.ToUnixTimeMilliseconds()
        };
    }
}

New library on offer for Serilog destructuring of NodaTime types

I've written a library which destructures NodaTime objects in order for them to be cleanly logged in Serilog. The destructuring policies were copied from NodaTime.Serialization.JsonNet, and the JSON created by Serilog after destructuring by my library is roundtrip deserializable by NodaTime.Serialization.JsonNet. This is backed by unit tests. The log entries are also much more compact and human readable than without the library.

I'd like to find the code a home and was wondering if you would review it for inclusion in this repo or elsewhere within the organisation? Strictly speaking it could be said that destructuring is the step before deserialization but it might be close enough?

If you're interested to take a look could you please let me know:

  • The desired project location and project and namespace names
  • Framework targets (I'm currently targetting .NET Standard 2.0 in the library proper and .NET Core 2.2 in the unit tests)
  • Whether I need to fork and then branch, or just fork

...and I'll get a PR put together.

NodaTime negative impact on serialisation perf

As raised in the nodatime repo


I'm not trying to be a pain about perf, but I've seen NodaTime appear in some new profile results, and on investigation, it seems that using ConfigureForNodaTime settings has a significant impact on Json.NET serialisation performance, even when you're not serialising NodaTime types.

I have a simple benchmark that does some variants of
JsonConvert.SerializeObject(new { tm = _instant }, _settings);

Sometimes _tm is set to a DateTime rather than a Instant, and I either do or don't include _settings in the call.

These are the results:

                           Method |       Mean |    Error |    StdDev | Scaled | ScaledSD |  Gen 0 | Allocated |
--------------------------------- |-----------:|---------:|----------:|-------:|---------:|-------:|----------:|
 SerializeDateTimeWithoutSettings |   890.2 ns | 11.56 ns |  10.81 ns |   1.00 |     0.00 | 0.3605 |   1.11 KB |
  SerializeInstantWithOutSettings | 1,719.1 ns | 34.18 ns |  33.57 ns |   1.93 |     0.04 | 0.5093 |   1.57 KB |
    SerializeDateTimeWithSettings | 3,286.9 ns | 64.51 ns | 104.17 ns |   3.69 |     0.12 | 0.4196 |    1.3 KB |
     SerializeInstantWithSettings | 2,863.0 ns | 57.10 ns |  72.21 ns |   3.22 |     0.09 | 0.5226 |   1.62 KB |

Obviously these aren't all serialising to the same text: the two DateTime versions are the same, but Instant only serialises properly if you include _settings - without it you get the members of Duration separately serialised.

It seems pretty obvious from the DotTrace picture below what the problem is - NodaConverterBase needs to find a smarter way to answer the CanConvert question than reaching for the reflection sledgehammer. The obvious thing feels like a static dictionary cache of results, but one would need to think a bit about concurrency and whether there was some pathological bazillion-types scenario which could cause it to explode in length.

I think this is quite important because of the way that it impacts non NodaTime types so badly.

nt2

Full support for DateTimeOffset

Is it possible to get full support for DateTimeOffset type?

The following unit test:

        [Fact]
        public void TestNodaDateTimeOffset()
        {
            var s = "{\"date\":\"2017-05-09T09:42:59.4570182+02:00\"}";
            var settings = new JsonSerializerSettings().ConfigureForNodaTime(DateTimeZonePrโ€Œโ€‹oviders.Serializatioโ€Œโ€‹n);
            var value = JsonConvert.DeserializeObject<TestClass>(s, settings);
        }

        private class TestClass
        {
            [JsonProperty("date")]
            public Instant Date { get; set; }
        }

Throws the following exception:

{NodaTime.Text.UnparsableValueException: The value string does not match a quoted string in the pattern. Value being parsed: '2017-05-09T09:42:59.4570182^+02:00'. (^ indicates error position.)
   at NodaTime.Text.ParseResult`1.GetValueOrThrow()
   at NodaTime.Serialization.JsonNet.NodaPatternConverter`1.ReadJsonImpl(JsonReader reader, JsonSerializer serializer)
   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)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
   at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings)
   at ApiApplication.Tests.Controllers.DemoAssetsControllerTests.TestDes() in D:\git\test\ApiApplication.Tests\Controllers\Tests.cs:line 217}

But it works if I use the date string "{\"date\":\"2017-05-09T07:42:59.4570182Z\"}"

I'm using the following csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>    
    <TargetFramework>netcoreapp1.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
    <PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.0.0" />
    <PackageReference Include="xunit" Version="2.2.0" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
  </ItemGroup>

</Project>

NodaTime Serialization ignores Required Attribute

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.

Support for Temporal JS serialization format

It looks like the ECMA/JavaScript world is about to accept the Temporal JS proposal, which has a slightly different serialization format than that used by nodatime. https://tc39.es/proposal-temporal/docs/iso-string-ext.html

In particular, for ZonedDateTime, the proposed Temporal object is going to serialization it to a format like 2007-12-03T10:15:30+01:00[Europe/Paris]

It doesnโ€™t look like it will be that difficult to write a custom converter to deserialize this to nodatimeโ€™s ZonedDateTime, but it would be nice if that was supported as an option out of the box given that I imagine the JS world (or at least the part of the JS world that is intentional about capturing time zone information) will start using this format.

Is there any plan to support that? (Or reason not to?)

Implement AOT support

Once we've sorted out support for AOT in the main NodaTime assemblies, we should do the same in this repo - it's more likely to be relevant really, given that serialization often involves reflection. Will wait until we've done it for the main assemblies first though.

HTTP POST - InvalidNodaDataException for null values (NewtonsoftJson)

Hello,

we have been using the NodaTime library for building ASP.NET Core REST APIs and we would like to handle incoming null values from the client more appropriately. We have observed, that null value for types such as LocalDateTime, Instant (and possibly others) results in an exception being thrown and HTTP 500 status code, for example: NodaTime.Utility.InvalidNodaDataException: Cannot convert null value to NodaTime.LocalDateTime.

On the other hand, when assigning a null value to the BCL DateTime property, HTTP 400 status code is returned with an error message Error converting value {null} to type 'System.DateTime'. From our point of view, this behaviour is more appropriate, because it is clearly the fault of the client, so the HTTP 400 makes more sense.

The following overview compares the difference in behaviour between the LocalDateTime and DateTime types (using HTTP POST request with enabled ConfigureForNodaTime(DateTimeZoneProviders.Tzdb))

The HTTP POST examples

{
    // *** NODA ***
     
    // 1) HTTP 200
    // localDateTime": "2022-01-01T10:00:00",
   
    // 2) HTTP 500, NodaTime.Utility.InvalidNodaDataException: Cannot convert null value to NodaTime.LocalDateTime
    // "localDateTime": null,

    // 3) HTTP 400, Cannot convert value to NodaTime.LocalDateTime
    // "localDateTime": false

    // 4) HTTP 400, Cannot convert value to NodaTime.LocalDateTime
    // "localDateTime": "abcd"

    // *** BCL ***

    // 5) HTTP 200
    // "dateTime": "2022-01-01T00:00:00Z"
   
    // 6) HTTP 400,   Error converting value {null} to type 'System.DateTime'.
    // "dateTime": null

    // 7) HTTP 400, Unexpected character encountered while parsing value: f
    // "dateTime": false

    // 8) HTTP 400, Could not convert string to DateTime: abcd.
    // "dateTime": "abcd"
}

Program.cs

using NodaTime;
using NodaTime.Serialization.JsonNet;

var builder = WebApplication.CreateBuilder(args);

// services
builder.Services.AddControllers().AddNewtonsoftJson(s =>
    s.SerializerSettings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb));

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();

// pipeline
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseAuthorization();
app.MapControllers();
app.Run();

Endpoint and model

namespace NodaNewtonsoft.Controllers;

[ApiController]
[Route("demo")]
public class DemoController : ControllerBase
{

    [HttpPost("noda")]
    public IActionResult Demo([FromBody] Request request)
    {
        return Ok(request);
    }
}

public class Request
{
    public LocalDateTime LocalDateTime { get; set; }
    public DateTime DateTime { get; set; }
}

The bottom line is, we do not want to return HTTP 500 for scenarios when the provided value for the LocalDateTime is null. Although making the property nullable fixes the issue, it does not seem like the right solution for the cases when the value must be provided by the client. Is this behaviour by design? Is there any recommended way to get the standard 400 Bad Request result for the null values, such as:

{
    "errors": {      
        "localDateTime": [
            // some example error message that does not exist right now
            "Cannot convert {null} to NodaTime.LocalDateTime"
        ]
    },
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "00-38367af21814a198e948f57c45737200-077bfffefaea8feb-00"
}

Thank you for your help and have a nice day.

Details

  • "Microsoft.AspNetCore.Mvc.NewtonsoftJson", Version="6.0.11"
  • "NodaTime", Version="3.1.5"
  • "NodaTime.Serialization.JsonNet", Version="3.0.1"
  • Target framework - net6.0

The runnable demo app is attached below.
NodaNewtonsoft.zip

Write up documentation on process for this repo

Example: the use of tags, all on one branch - because we'll have multiple projects.
Oh, and avoiding what I've already done in terms of tag names... make the tag name include the project.

Serialization of Nullable<AnnualDate> with System.Text.Json

Using the preview of System.Text.Json serialization, and Asp.Net Core 3.1, I'm getting a default serialization for nullable AnnualDate values when they are not null:

{
    "annualDate": {
        "month": 1,
        "day": 1
    },
    "nullableAnnualDateWithValue": {
        "hasValue": true,
        "value": {
            "month": 1,
            "day": 1
        }
    },
    "nullableAnnualDateWithoutValue": null
}

This is different to other composite values such as Interval, which serializes as follows:

{
    "interval": {
        "start": "1970-01-01T00:00:00Z",
        "end": "1970-01-01T00:00:00Z"
    },
    "nullableIntervalWithValue": {
        "start": "1970-01-01T00:00:00Z",
        "end": "1970-01-01T00:00:00Z"
    },
    "nullableIntervalWithoutValue": null
}

Is there any issue preventing the AnnualDate struct being serialized like the others?

Json.NET Period deserialization does not work

The deserialization of Period seems broken with the current release: the following test fails at the second โ€œassertโ€œ, though I'd assume that both methods yield the same Period.

[Fact]
public void CanParsePeriod()
{
    var periodString = "PT30M";
    
    var test1 = PeriodPattern.NormalizingIso.Parse("PT30M").Value;
    Assert.Equal(Period.FromMinutes(30), test1);

    var test2 = JsonConvert.DeserializeObject<Period>(periodString,
        new[] {NodaConverters.NormalizingIsoPeriodConverter});
    Assert.Equal(Period.FromMinutes(30), test2);

}
Newtonsoft.Json.JsonReaderException : Unexpected character encountered while parsing value: P. Path '', line 0, position 0.
   at Newtonsoft.Json.JsonTextReader.ParseValue()
   at Newtonsoft.Json.JsonReader.ReadForType(JsonContract contract, Boolean hasConverter)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
   at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonConverter[] converters)

Fix System.Text.Json tests on Travis

Now that I've updated the SDK, we're still getting a lot of test failures, which look like they're to do with unnecessary escaping:

$ dotnet test src/NodaTime.Serialization.Test
Test run for /home/travis/build/nodatime/nodatime.serialization/src/NodaTime.Serialization.Test/bin/Debug/netcoreapp2.0/NodaTime.Serialization.Test.dll(.NETCoreApp,Version=v2.0)
Microsoft (R) Test Execution Command Line Tool Version 16.2.0-preview-20190606-02
Copyright (c) Microsoft Corporation.  All rights reserved.
Starting test execution, please wait...
  X OffsetConverter [54ms]
  Error Message:
     Expected string length 8 but was 13. Strings differ at index 1.
  Expected: ""+05:30""
  But was:  ""\\u002b05:30""
  ------------^
  Stack Trace:
     at NodaTime.Serialization.Test.SystemText.TestHelper.AssertConversions[T](T value, String expectedJson, JsonSerializerOptions options) in /home/travis/build/nodatime/nodatime.serialization/src/NodaTime.Serialization.Test/SystemText/TestHelper.cs:line 28
   at NodaTime.Serialization.Test.SystemText.TestHelper.AssertConversions[T](T value, String expectedJson, JsonConverter converter) in /home/travis/build/nodatime/nodatime.serialization/src/NodaTime.Serialization.Test/SystemText/TestHelper.cs:line 22
   at NodaTime.Serialization.Test.SystemText.NodaConvertersTest.OffsetConverter() in /home/travis/build/nodatime/nodatime.serialization/src/NodaTime.Serialization.Test/SystemText/NodaConvertersTest.cs:line 23
Test run in progress.  X OffsetDateTimeConverter_WholeHours [1ms]
  Error Message:
     Expected string length 37 but was 42. Strings differ at index 30.
  Expected: ""2012-01-02T03:04:05.123456789+05:00""
  But was:  ""2012-01-02T03:04:05.123456789\\u002b05:00""
  -----------------------------------------^
  Stack Trace:
     at NodaTime.Serialization.Test.SystemText.TestHelper.AssertConversions[T](T value, String expectedJson, JsonSerializerOptions options) in /home/travis/build/nodatime/nodatime.serialization/src/NodaTime.Serialization.Test/SystemText/TestHelper.cs:line 28
   at NodaTime.Serialization.Test.SystemText.TestHelper.AssertConversions[T](T value, String expectedJson, JsonConverter converter) in /home/travis/build/nodatime/nodatime.serialization/src/NodaTime.Serialization.Test/SystemText/TestHelper.cs:line 22
   at NodaTime.Serialization.Test.SystemText.NodaConvertersTest.OffsetDateTimeConverter_WholeHours() in /home/travis/build/nodatime/nodatime.serialization/src/NodaTime.Serialization.Test/SystemText/NodaConvertersTest.cs:line 174
Test run in progress.  X ZonedDateTimeConverter [7ms]
  Error Message:
     Expected string length 38 but was 48. Strings differ at index 20.
  Expected: ""2012-10-28T01:30:00+01 Europe/London""
  But was:  ""2012-10-28T01:30:00\\u002b01 Europe\\u002fLondon""
  -------------------------------^
  Stack Trace:
     at NodaTime.Serialization.Test.SystemText.TestHelper.AssertConversions[T](T value, String expectedJson, JsonSerializerOptions options) in /home/travis/build/nodatime/nodatime.serialization/src/NodaTime.Serialization.Test/SystemText/TestHelper.cs:line 28
   at NodaTime.Serialization.Test.SystemText.TestHelper.AssertConversions[T](T value, String expectedJson, JsonConverter converter) in /home/travis/build/nodatime/nodatime.serialization/src/NodaTime.Serialization.Test/SystemText/TestHelper.cs:line 22
   at NodaTime.Serialization.Test.SystemText.NodaConvertersTest.ZonedDateTimeConverter() in /home/travis/build/nodatime/nodatime.serialization/src/NodaTime.Serialization.Test/SystemText/NodaConvertersTest.cs:line 155
Test run in progress.  X Serialize [1ms]
  Error Message:
     Expected string length 21 but was 26. Strings differ at index 8.
  Expected: ""America/Los_Angeles""
  But was:  ""America\\u002fLos_Angeles""
  -------------------^
  Stack Trace:
     at NodaTime.Serialization.Test.SystemText.NodaDateTimeZoneConverterTest.Serialize() in /home/travis/build/nodatime/nodatime.serialization/src/NodaTime.Serialization.Test/SystemText/NodaDateTimeZoneConverterTest.cs:line 30
Test run in progress.  X RoundTrip [1ms]
  Error Message:
     Expected string length 23 but was 28. Strings differ at index 11.
  Expected: ""2012-01-02/2013-06-07""
  But was:  ""2012-01-02\\u002f2013-06-07""
  ----------------------^
  Stack Trace:
     at NodaTime.Serialization.Test.SystemText.TestHelper.AssertConversions[T](T value, String expectedJson, JsonSerializerOptions options) in /home/travis/build/nodatime/nodatime.serialization/src/NodaTime.Serialization.Test/SystemText/TestHelper.cs:line 28
   at NodaTime.Serialization.Test.SystemText.NodaIsoDateIntervalConverterTest.RoundTrip() in /home/travis/build/nodatime/nodatime.serialization/src/NodaTime.Serialization.Test/SystemText/NodaIsoDateIntervalConverterTest.cs:line 28
Test run in progress.  X Serialize_InObject [1ms]
  Error Message:
     Expected string length 36 but was 41. Strings differ at index 23.
  Expected: "{"Interval":"2012-01-02/2013-06-07"}"
  But was:  "{"Interval":"2012-01-02\\u002f2013-06-07"}"
  ----------------------------------^
  Stack Trace:
     at NodaTime.Serialization.Test.SystemText.NodaIsoDateIntervalConverterTest.Serialize_InObject() in /home/travis/build/nodatime/nodatime.serialization/src/NodaTime.Serialization.Test/SystemText/NodaIsoDateIntervalConverterTest.cs:line 50
Test run in progress.  X RoundTrip [1ms]
  Error Message:
     Expected string length 56 but was 61. Strings differ at index 24.
  Expected: ""2012-01-02T03:04:05.67Z/2013-06-07T08:09:10.123456789Z""
  But was:  ""2012-01-02T03:04:05.67Z\\u002f2013-06-07T08:09:10.123456789Z""
  -----------------------------------^
  Stack Trace:
     at NodaTime.Serialization.Test.SystemText.TestHelper.AssertConversions[T](T value, String expectedJson, JsonSerializerOptions options) in /home/travis/build/nodatime/nodatime.serialization/src/NodaTime.Serialization.Test/SystemText/TestHelper.cs:line 28
   at NodaTime.Serialization.Test.SystemText.NodaIsoIntervalConverterTest.RoundTrip() in /home/travis/build/nodatime/nodatime.serialization/src/NodaTime.Serialization.Test/SystemText/NodaIsoIntervalConverterTest.cs:line 28
Test run in progress.  X RoundTrip_Infinite [12ms]
  Error Message:
     Expected string length 33 but was 38. Strings differ at index 1.
  Expected: ""/2013-06-07T08:09:10.123456789Z""
  But was:  ""\\u002f2013-06-07T08:09:10.123456789Z""
  ------------^
  Stack Trace:
     at NodaTime.Serialization.Test.SystemText.TestHelper.AssertConversions[T](T value, String expectedJson, JsonSerializerOptions options) in /home/travis/build/nodatime/nodatime.serialization/src/NodaTime.Serialization.Test/SystemText/TestHelper.cs:line 28
   at NodaTime.Serialization.Test.SystemText.NodaIsoIntervalConverterTest.RoundTrip_Infinite() in /home/travis/build/nodatime/nodatime.serialization/src/NodaTime.Serialization.Test/SystemText/NodaIsoIntervalConverterTest.cs:line 35
Test run in progress.  X Serialize_InObject [2ms]
  Error Message:
     Expected string length 56 but was 61. Strings differ at index 33.
  Expected: "{"Interval":"2012-01-02T03:04:05Z/2013-06-07T08:09:10Z"}"
  But was:  "{"Interval":"2012-01-02T03:04:05Z\\u002f2013-06-07T08:09:10Z"}"
  --------------------------------------------^
  Stack Trace:
     at NodaTime.Serialization.Test.SystemText.NodaIsoIntervalConverterTest.Serialize_InObject() in /home/travis/build/nodatime/nodatime.serialization/src/NodaTime.Serialization.Test/SystemText/NodaIsoIntervalConverterTest.cs:line 73
Test run in progress.Test Run Failed.
Total tests: 244
     Passed: 235
     Failed: 9
 Total time: 3.1783 Seconds
The command "dotnet test src/NodaTime.Serialization.Test" exited with 1.

(There's also a build failure around SourceLink, but we can look at that separately.)

Issue with deserializing NodaTime types

Hi! I faced an issue which is blocking me.
I'm using this helper to serialize/deserialize data

public static class Helpers
    {
        public static string ToJson(this object data)
        {
            var settings = new JsonSerializerSettings();
            settings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
            string json = JsonConvert.SerializeObject(data, settings);
            return json;
        }

        public static T FromJson<T>(this string jsonText)
        {
            var settings = new JsonSerializerSettings();
            settings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
            var obj = JsonConvert.DeserializeObject<T>(jsonText);
            return obj;
        }

    }

and a simple test which fails on an object deserialization.

public class TestClass2
    {
        public LocalDate A { get; set; }
        public LocalDateTime B { get; set; }
        public Instant C { get; set; }
    }

    public class JsonSerializerTests
    {
        [Fact]
        public async Task Test1()
        {
            // arrange
            var obj = new TestClass2
            {
                A = LocalDate.FromDateTime(DateTime.Today),
                B = LocalDateTime.FromDateTime(DateTime.Now),
                C = SystemClock.Instance.GetCurrentInstant()
            };



            //act
            var jsonData = obj.ToJson();
            var deserializedObj = jsonData.FromJson<TestClass2>();

            //assert
            obj.A.Should().Be(deserializedObj.A);
            obj.B.Should().Be(deserializedObj.B);
            obj.C.Should().Be(deserializedObj.C);
        }
    }

Release 2.1.0

Please release 2.1.0 because no issues with beta for 4 month.

Posting ZonedDateTime Json return Bad Request

I'm not sure if it is issue or I'm doing something wrong.
I have an API that return json of ZonedDateTime, when I used that json to send it as query string to the Api it always returns bad request.

I tried to send the value of that date by doing zonedDateTime.ToString() as a query string and it worked , you can find that below

The returned Json from the Api :
2020-05-22T04:37:50.6817749+03 Asia/Riyadh

Sending it as a query string after encoding,always returns bad request:
2020-05-22T04:37:50.6817749%2B03%20Asia/Riyadh --> 2020-05-22T04:37:50.6817749+03 Asia/Riyadh

Sending as the return value of ToString() method it worked:
2020-05-22T04:37:50%20Asia/Riyadh%20(%2B03) --> 2020-05-22T04:37:50 Asia/Riyadh (+03)

Packages versions that I used:
Asp.net core version 5.0
NodeTime version 3.0.0-beta02
NodaTime.Serialization.SystemTextJson version 1.0.0-beta02

Class Startup {
public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers().AddJsonOptions(options =>
        {
            
options.JsonSerializerOptions.ConfigureForNodaTime(NodaTime.DateTimeZoneProviders.Tzdb);
            options.JsonSerializerOptions.WithIsoDateIntervalConverter();
            options.JsonSerializerOptions.WithIsoIntervalConverter();
        });
        services.AddScoped<NodaTime.IClock, NodaTime.SystemClock>(p => 
NodaTime.SystemClock.Instance);
        
    }
}

ApiController{

public  ZonedDateTime Get(string zoneId, ZonedDateTime date)
    {
        zoneId ??= DateTimeZoneProviders.Tzdb.GetSystemDefault().Id;
        var zone = DateTimeZoneProviders.Tzdb[zoneId];
        var now = new ZonedDateTime(clock.GetCurrentInstant(), zone);
        return now;
    }

}

`
Appreciate any help & Thanks

NodaTime v3 support

Looks like NodaTime 3 is not supported while nuget package claims that it is. This results in a runtime binding resolution exception:

System.IO.FileLoadException: Could not load file or assembly 'NodaTime, Version=2.4.8.0, Culture=neutral, PublicKeyToken=4226afe0d9b296d1' 
or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)
File name: 'NodaTime, Version=2.4.8.0, Culture=neutral, PublicKeyToken=4226afe0d9b296d1'

From nodatime.serialization.jsonnet.nuspec:

<group targetFramework=".NETStandard2.0">
    ...
    <dependency id="NodaTime" version="2.4.8" exclude="Build,Analyzers" />
</group>

My understanding is that "2.4.8" means 2.4.8 or higher (minimum version, inclusive). And instead it should be something like "(,2.4.8]" (maximum version, inclusive). In which case msbuild should issue a warning when building code that references both NodaTime (v3) and NodaTime.Serialization.JsonNet. So a manual binding redirect can be added to avoid runtime exception. At least until NodaTime.Serialization.JsonNet starts referencing NodaTime v3 package.

New project: NodaTime.Protobuf

Now that 2.0 is out (well, building) and serialization is in a different repo, we can start another serialization project: integration with Google.Protobuf.

This will provide simple conversions between the Noda Time Instant/Duration types and the Protobuf Timestamp/Duration types, as well as possibly the types in Google.Type (TimeOfDay, Date).

Add case-insensitive deserialization for JSON

Currently deserialization is always case-sensitive without any option to make it case-insensitive. This is contrary to how the underlying Newtonsoft.Json operates which is case-insensitive by default.
Result is that switching casing on serialization side breaks deserialization side.
Especially when upgrading from an older NodaTime (e.g. 1.4 which was always PascalCase) to latest version (2.4), which now respects JsonSerializerSettings.ContractResolver setting.
Example: Interval class has Start and End properties that are case-sensitive when deserializing.

Support for System.Text.Json

Given that .net core 3.0 is around the corner I think it would be great to add support for System.Text.Json serializer.
I've been playing around with it for some time and am willing to submit a PR.

Empty String Conversion to Nullable missing when switching from NodaTime.Serialization.JsonNet to NodaTime.Serialization.SystemTextJson

Our project was previously using newtonsoft to serialize/deserialize JSON for MVC and we used the extension method provided in NodaTime.Serialization.JsonNet to configure nodatime. We recently tried switching over to NodaTime.Serialization.SystemTextJson and everything looked great at first, but then we noticed a problem.

Previously, empty strings would be converted to null for nodatime types such as LocalTime? and LocalDate? without any configuration necessary.

With the SystemTextJson extension method (ConfigureForNodaTime), it now fails for us at the model binding layer when encountering an empty string and the entire model returned to the controller is null.

Are there any plans to bring this functionality back to the SystemTextJson converters? I noticed the old converter base class does a sort of conversion for nullable types when an empty string is encountered, but the new one does not do such a conversion.

JSON deserialization throws NodaTime.Text.UnparsableValueException

Given

class Program
{
    static void Main(string[] args)
    {
        var json = @"{ ""value"": ""2012-12-12"" }";
        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
        var model = JsonConvert.DeserializeObject<Model>(json, settings);
    }
}

class Model
{
    public ZonedDateTime Value { get; set; }
}

The DeserializeObject call throws the NodaTime specific NodaTime.Text.UnparsableValueException. In ASP.NET Core MVC, this results in a 500. I would have expected a JsonSerializationException that includes the property name. I believe ASP.NET Core MVC looks for that exception to formulate a 400 response instead.

I know this is perhaps a breaking behavior change that could go with either this project or JSON.Net.

Thanks!

System.Text.Json and Custom Formats in Model Binding

Hi there!

I've spent the whole day trying to solve or bypass the issue I described below, any help or confirmation about existing bug will be very appreciated.
Brief description:

Previously, I figured out (thank you for pointing me to the right way, again) how to add custom converter/formats for further deserialization. So, I have the following configuration:

public static NodaJsonSettings GetNodaJsonSettings()
        {
            var localTimePatternBuilder = new CompositePatternBuilder<LocalTime>();
            localTimePatternBuilder.Add(LocalTimePattern.CreateWithInvariantCulture("HH:mm"), time => true);
            localTimePatternBuilder.Add(LocalTimePattern.ExtendedIso, time => true);
            var localTimePattern = localTimePatternBuilder.Build();

            var localDateTimePatternBuilder = new CompositePatternBuilder<LocalDateTime>();
            localDateTimePatternBuilder.Add(LocalDateTimePattern.CreateWithInvariantCulture("yyyy-MM-dd HH:mm"), time => true);
            localDateTimePatternBuilder.Add(LocalDateTimePattern.CreateWithInvariantCulture("yyyy-MM-ddTHH:mm"), time => true);
            localDateTimePatternBuilder.Add(LocalDateTimePattern.ExtendedIso, time => true);
            var localDateTimePattern = localDateTimePatternBuilder.Build();

            var instantPatternBuilder = new CompositePatternBuilder<Instant>();
            instantPatternBuilder.Add(InstantPattern.CreateWithInvariantCulture("yyyy-MM-dd HH:mm"), time => true);
            instantPatternBuilder.Add(InstantPattern.CreateWithInvariantCulture("yyyy-MM-ddTHH:mm"), time => true);
            instantPatternBuilder.Add(InstantPattern.ExtendedIso, instant => true);
            var instantPattern = instantPatternBuilder.Build();

            var nodaJsonSettings = new NodaJsonSettings(DateTimeZoneProviders.Tzdb)
            {
                InstantConverter = new NodaTime.Serialization.SystemTextJson.NodaPatternConverter<Instant>(instantPattern),
                LocalTimeConverter = new NodaTime.Serialization.SystemTextJson.NodaPatternConverter<LocalTime>(localTimePattern),
                LocalDateTimeConverter = new NodaTime.Serialization.SystemTextJson.NodaPatternConverter<LocalDateTime>(localDateTimePattern),
            };

            return nodaJsonSettings;
        }

And everything worked fine with Newtonsoft.JsonNET, but now we started a migration to System.Text.Json
Default converters are working fine, but not custom ones.

I tried many ways on how to fix the issue, but it seems like on Model Binding all custom formats are ignored despite it exists in configuration.

I've read several similar issue reports and did as many bypassed as possible to push the only one correct JsonSettingsOptions configuration:

var jsonSerializerOptions= new JsonSerializerOptions()
            {
                PropertyNameCaseInsensitive = true,
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
                DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
                DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
                ReferenceHandler = ReferenceHandler.IgnoreCycles,
                WriteIndented = true
            };
            jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
            jsonSerializerOptions.ConfigureForNodaTime(SerializationExtension.GetNodaJsonSettings());
            services.AddSingleton(s => jsonSerializerOptions);

services.AddControllers(
                    config =>
                    {
                        config.Filters.Add<SpotoExceptionFilter>();

                        var inputFormatter =
                            config.InputFormatters.OfType<SystemTextJsonInputFormatter>().FirstOrDefault();

                        if (inputFormatter != null)
                        {
                            SerializationExtension.CopyJsonSerializerOptions(inputFormatter.SerializerOptions, jsonSerializerOptions);
                        }

                        var outputFormatter =
                            config.OutputFormatters.OfType<SystemTextJsonInputFormatter>().FirstOrDefault();

                        if (outputFormatter != null)
                        {
                            SerializationExtension.CopyJsonSerializerOptions(outputFormatter.SerializerOptions, jsonSerializerOptions);
                        }
                    })
                .AddJsonOptions(o => SerializationExtension.CopyJsonSerializerOptions(o.JsonSerializerOptions, jsonSerializerOptions));

Where there is a helper method:

public static void CopyJsonSerializerOptions(JsonSerializerOptions targetOptions, JsonSerializerOptions sourceOptions)
        {
            // Notes. A hacky but necessary approach, because we are given a pre-built JsonSerializerOptions object
            // that can't be replaced, therefore we must copy the various setting into it.
            targetOptions.AllowTrailingCommas = sourceOptions.AllowTrailingCommas;
            targetOptions.DefaultBufferSize = sourceOptions.DefaultBufferSize;
            targetOptions.DefaultIgnoreCondition = sourceOptions.DefaultIgnoreCondition;
            targetOptions.DictionaryKeyPolicy = sourceOptions.DictionaryKeyPolicy;
            targetOptions.Encoder = sourceOptions.Encoder;
            targetOptions.IgnoreReadOnlyFields = sourceOptions.IgnoreReadOnlyFields;
            targetOptions.IncludeFields = sourceOptions.IncludeFields;
            targetOptions.MaxDepth = sourceOptions.MaxDepth;
            targetOptions.NumberHandling = sourceOptions.NumberHandling;
            targetOptions.PropertyNameCaseInsensitive = sourceOptions.PropertyNameCaseInsensitive;
            targetOptions.PropertyNamingPolicy = sourceOptions.PropertyNamingPolicy;
            targetOptions.ReadCommentHandling = sourceOptions.ReadCommentHandling;
            targetOptions.ReferenceHandler = sourceOptions.ReferenceHandler;
            targetOptions.UnknownTypeHandling = sourceOptions.UnknownTypeHandling;
            targetOptions.WriteIndented = sourceOptions.WriteIndented;
            targetOptions.DefaultIgnoreCondition = sourceOptions.DefaultIgnoreCondition;

            // Re-use the same converter instances; this is OK because their current parent
            // SystemTextJsonInputFormatter is about to be discarded.
            targetOptions.Converters.Clear();
            foreach (var jsonConverter in sourceOptions.Converters)
            {
                targetOptions.Converters.Add(jsonConverter);
            }
        }

But nothing helped.

And when I send a request to a test endpoint:

public class TestReq
    {
        public LocalDateTime datetime { get; set; } // 2023-11-01 12:15

        public LocalTime time { get; set; } // 13:45
    }

public IActionResult Test([FromQuery] TestReq req)

I'm getting and wrong model exception, but when used Newtonsoft.JsonNet it worked like a charm.

@jskeet Is it a known bug or there is a way how bypass it? Any info will be very welcomed.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.