Convert UnixTime to DateTimeOffset with a custom System.Text.Json Converter

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!