Convert UnixTime to DateTimeOffset with a custom System.Text.Json Converter
Some APIs do not follow standards, ignore ISO8601 and return UnixTime. This is not nice, but can be easily fixed with a custom converter for System.Text.Json.
The converter is quite simple. It expects a number and converts it into a DateTimeOffset when reading - and vice versa when writing. For compatibility reasons, the converter should not only support the resolution of seconds, because some return Unix Time as milliseconds.
/// <summary>
/// Converts Unix time to nullable DateTimeOffset.
/// </summary>
public class UnixToNullableDateTimOffsetConverter : JsonConverter<DateTimeOffset?>
{
/// <summary>
/// Minimum Unix time in seconds.
/// </summary>
private static readonly long s_unixMinSeconds = DateTimeOffset.MinValue.ToUnixTimeSeconds();
/// <summary>
/// Maximum Unix time in seconds.
/// </summary>
private static readonly long s_unixMaxSeconds = DateTimeOffset.MaxValue.ToUnixTimeSeconds();
/// <summary>
/// Determines if the time should be formatted as seconds. False if resolved as milliseconds.
/// </summary>
public bool FormatAsSeconds { get; init; } = true;
/// <summary>
/// Reads and converts the JSON to type T.
/// </summary>
public override DateTimeOffset? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
try
{
if (reader.TryGetInt64(out long time))
{
// If FormatAsSeconds is not specified, the correct type is derived depending on whether
// the value can be represented as seconds within the .NET DateTimeOffset min/max range 0001-1-1 to 9999-12-31.
// Since this is a 64-bit value, the Unixtime in seconds may exceed
// the 32-bit min/max restrictions 1/1/1970-1-1 to 1/19/2038-1-19.
if (FormatAsSeconds || !FormatAsSeconds && time > s_unixMinSeconds && time < s_unixMaxSeconds)
{
return DateTimeOffset.FromUnixTimeSeconds(time);
}
return DateTimeOffset.FromUnixTimeMilliseconds(time);
}
}
catch
{
// TryGetInt64 still can throw exceptions if valid is invalid (e.g. no number)
}
return null;
}
/// <summary>
/// Writes the converted value to JSON.
/// </summary>
public override void Write(Utf8JsonWriter writer, DateTimeOffset? value, JsonSerializerOptions options)
{
if (value is DateTimeOffset date)
{
if (FormatAsSeconds)
{
writer.WriteNumberValue(date.ToUnixTimeSeconds());
}
else
{
writer.WriteNumberValue(date.ToUnixTimeMilliseconds());
}
}
else
{
writer.WriteNullValue();
}
}
}
The obligatory test
public class UnixToNullableDateTimOffsetConverterTests
{
private readonly UnixToNullableDateTimOffsetConverter _converter = new();
[Fact]
public void TestRead()
{
string json = "1619827200"; // Unix timestamp for 2021-05-01
Utf8JsonReader reader = new(System.Text.Encoding.UTF8.GetBytes(json));
reader.Read();
DateTimeOffset? result = _converter.Read(ref reader, typeof(DateTimeOffset?), new JsonSerializerOptions());
Assert.Equal(new DateTimeOffset(2021, 5, 1, 0, 0, 0, TimeSpan.Zero), result);
}
[Fact]
public void TestWrite()
{
ArrayBufferWriter<byte> buffer = new();
Utf8JsonWriter writer = new(buffer);
DateTimeOffset date = new(2021, 5, 1, 0, 0, 0, TimeSpan.Zero);
_converter.Write(writer, date, new JsonSerializerOptions());
writer.Flush();
string json = System.Text.Encoding.UTF8.GetString(buffer.WrittenSpan);
Assert.Equal("1619827200", json);
}
}
Have fun!