Serializing .NET 6s new DateOnly to JSON
By Martijn Storck
In june 2021, a new System.DateOnly
type was introduced in the .NET 6 Preview 4. This type is convenient
for storing a date, consisting of year, month and date without time or timezone information. The benefit is increased
type safety when integrating with APIs that use a similar date type, avoiding the need to parse dates into a DateTime
with the time set to 00:00:00 +0000
by convention.
Unfortunately, the integration with APIs also brings a challenge since there is no standard to serialize dates as
JSON. In the absence of a standard, System.Text.Json
simply does not know how to serialize a date. Consider the
following model and controller for a birthday calendar:
public class Birthday
{
public DateOnly Date { get; set; }
public string Name { get; set; }
}
// …
public class BirthdaysController : ControllerBase
{
// …
public ActionResult<IEnumerable<Birthday>> GetBirthdays(string dateStr)
{
var date = DateOnly.ParseExact(dateStr, "yyyy-MM-dd", CultureInfo.InvariantCulture);
return _birthdayRepository.Birthdays.Where(b => b.Date == date).ToList();
}
}
Calling this action will lead to the following error:
---> System.NotSupportedException: Serialization and deserialization of 'System.DateOnly' instances are not supported.
at System.Text.Json.Serialization.Converters.UnsupportedTypeConverter`1.Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
The serialization magic in ASP.NET Core that allows us to quickly develop strongly typed APIs breaks down in the absence of a standard for JSON serialization.
Custom JsonConvertor using attributes
Fortunately, we can easily define a custom serialization for our DateOnly field,
by applying the System.Text.Json.Serialization.JsonConverter
attribute in our model:
using System.Text.Json.Serialization;
public class Birthday
{
[JsonConverter(typeof(DateOnlyJsonConverter))]
public DateOnly Date { get; set; }
public string Name { get; set; }
}
The implementation of this converter uses the same yyyy-MM-dd convention as our controller action above:
public class DateOnlyJsonConverter : JsonConverter<DateOnly>
{
private const string Format = "yyyy-MM-dd";
public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return DateOnly.ParseExact(reader.GetString()!, Format, CultureInfo.InvariantCulture);
}
public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString(Format, CultureInfo.InvariantCulture));
}
}
Now the Date property will be (de)serialized to the specified format.
Alternative: Registering a global converter
Instead of registering the converter on a field level using the attribute, it is also possible to register the
custom DateOnly converter globally to make it apply to all controllers. This is done using IMvcBuilder.AddJsonOptions
which is added to the AddControllers
in Program.cs
as follows:
services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new DateOnlyJsonConverter());
});
The choice of attribute vs global converter depends on your application and domain.
Bringing Swagger documentation in line
Finally, one of the perks of ASP.NET Core is the inclusion of Swashbuckle by default to generate Swagger docs based on the controller definitions. However, by default it will show the schema for our Date field as follows:
{
"date": {
"year": 0,
"month": 0,
"day": 0,
"dayOfWeek": 0
}
}
To make the documentation reflect the correct format, add the following to the AddSwaggerGen()
call in Program.cs
:
builder.Services.AddSwaggerGen(options =>
options.MapType<DateOnly>(() => new OpenApiSchema
{
Type = "string",
Format = "date",
Example = new OpenApiString("2022-01-01")
})
);
Restart the app and now the schema wil reflect the correct example:
{ "date": "2022-01-01" }
Kalender foto gemaakt door freepik - nl.freepik.com