Search code examples
c#asp.net-corejson.netnodetime

NodaTime ZonedDateTime error on serializing in ASP.NET Core controller


I have an object as a body on POST request in my ASP.NET Core 8 application. The object is not serialized, and we found out that the property that is causing all is one with type ZonedDateTime and it has a value:

"completedTimestamp": "2023-05-18T14:39:55.303+02:00[Europe/Budapest]",

We are using Newtonsodt.Json library and configured the serializer settings as:

public static IMvcBuilder ConfigureApiBehaviorOptions(this IMvcBuilder builder)
{
    builder.AddNewtonsoftJson(options => 
    {
        var pattern = ZonedDateTimePattern.CreateWithInvariantCulture("yyyy'-'MM'-'dd'T'HH':'mm':'ss;FFFo<Z+HH:mm>'['z']'", DateTimeZoneProviders.Serialization);

        options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Ignore;
        options.SerializerSettings.DateParseHandling = DateParseHandling.None;
        options.SerializerSettings.Converters.Add(new NodaPatternConverter<ZonedDateTime>(pattern));
    });

    return builder;
}

When the request hits the controller my object is null, if I remove all properties of ZonedDateTime it got mapped.

If I try this with a a console app it works. My console app:

var pattern = ZonedDateTimePattern.CreateWithInvariantCulture(
        "yyyy'-'MM'-'dd'T'HH':'mm':'ss;FFFo<Z+HH:mm>'['z']'",
        DateTimeZoneProviders.Serialization
    );

var settings = new JsonSerializerSettings();
settings.DateParseHandling = DateParseHandling.None;
settings.Converters.Add(new NodaPatternConverter<ZonedDateTime>(pattern));

var json = "{\r\n\t\"completedTimestamp\": \"2023-05-18T14:39:55.303+02:00[Europe/Budapest]\"\r\n}";

var data = JsonConvert.DeserializeObject<Data>(json, settings);

Solution

  • Below is sample code you could try:

    Converters/ZonedDateTimeConverter.cs:

    using Newtonsoft.Json;
    using NodaTime;
    using NodaTime.Text;
    using System;
    
    namespace NodaTimeSample.Converters
    {
        public class ZonedDateTimeConverter : JsonConverter<ZonedDateTime>
        {
            private readonly ZonedDateTimePattern _pattern;
    
            public ZonedDateTimeConverter()
            {
                _pattern = ZonedDateTimePattern.CreateWithInvariantCulture(
                    "yyyy'-'MM'-'dd'T'HH':'mm':'ss;FFFo<Z+HH:mm>'['z']'",
                    DateTimeZoneProviders.Tzdb);
            }
    
            public override void WriteJson(JsonWriter writer, ZonedDateTime value, JsonSerializer serializer)
            {
                writer.WriteValue(_pattern.Format(value));
            }
    
            public override ZonedDateTime ReadJson(JsonReader reader, Type objectType, ZonedDateTime existingValue, bool hasExistingValue, JsonSerializer serializer)
            {
                var text = reader.Value?.ToString();
                Console.WriteLine($"Attempting to parse: {text}");
                var result = _pattern.Parse(text);
    
                if (result.Success)
                {
                    return result.Value;
                }
    
                Console.WriteLine($"Failed to parse ZonedDateTime: {text}");
                throw new JsonSerializationException($"Invalid ZonedDateTime format: {text}");
            }
        }
    }
    

    Data.cs:

    using NodaTime;
    
    namespace NodaTimeSample.Models
    {
        public class Data
        {
            public ZonedDateTime CompletedTimestamp { get; set; }
        }
    }
    

    MyController:

    using Microsoft.AspNetCore.Mvc;
    using NodaTimeSample.Models;
    
    namespace NodaTimeSample.Controllers
    {
        [ApiController]
        [Route("[controller]")]
        public class MyController : ControllerBase
        {
            [HttpPost]
            public IActionResult Post([FromBody] Data data)
            {
                if (data == null)
                {
                    Console.WriteLine("Data is null");
                    return BadRequest("Data is null");
                }
    
                Console.WriteLine($"Received data: {data.CompletedTimestamp}");
                return Ok(data);
            }
        }
    }
    

    Program:

    using Microsoft.AspNetCore.Mvc;
    using Newtonsoft.Json;
    using NodaTime;
    using NodaTime.Serialization.JsonNet;
    using NodaTime.Text;
    using NodaTimeSample.Converters;
    
    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.AddControllers()
            .AddNewtonsoftJson(options =>
            {
                var pattern = ZonedDateTimePattern.CreateWithInvariantCulture(
                    "yyyy'-'MM'-'dd'T'HH':'mm':'ss;FFFo<Z+HH:mm>'['z']'",
                    DateTimeZoneProviders.Serialization
                );
    
                options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Ignore;
                options.SerializerSettings.DateParseHandling = DateParseHandling.None;
                options.SerializerSettings.Converters.Add(new NodaPatternConverter<ZonedDateTime>(pattern));
            });
    
    
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    
    var app = builder.Build();
    
    if (app.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI();
    }
    
    app.UseHttpsRedirection();
    app.UseAuthorization();
    app.MapControllers();
    app.Run();
    

    Result:

    enter image description here