Skip to content

Instantly share code, notes, and snippets.

@akunzai
Last active February 22, 2021 16:49
Show Gist options
  • Save akunzai/7f4eec3ac912f6bb540d6d10d7d6bd34 to your computer and use it in GitHub Desktop.
Save akunzai/7f4eec3ac912f6bb540d6d10d7d6bd34 to your computer and use it in GitHub Desktop.
System.Text.Json deserialize legacy JSON date workaround
using System.Buffers;
using System.Buffers.Text;
using System.Globalization;
using System.Reflection;
using System.Text.RegularExpressions;
namespace System.Text.Json.Serialization
{
// https://github.com/dotnet/runtime/issues/30776
public class MicrosoftDateTimeJsonConverter : JsonConverterFactory
{
private static readonly DateTimeOffset _epoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
private static readonly Regex _regex = new Regex(@"^\\?/Date\((-?\d+)(?:([+-])(\d{2})(\d{2}))?\)\\?/$",
RegexOptions.CultureInvariant | RegexOptions.Compiled);
private static readonly byte[] _start = new byte[] {0x5C, 0x2F, 0x44, 0x61, 0x74, 0x65, 0x28};
private static readonly byte[] _end = new byte[] {0x29, 0x5C, 0x2F};
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTimeOffset);
}
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return typeToConvert == typeof(DateTime)
? new MicrosoftDateTimeConverter()
: new MicrosoftDateTimeOffsetConverter();
}
private static DateTimeOffset ReadCore(ref Utf8JsonReader reader, Type typeToConvert)
{
if (reader.TryGetDateTimeOffset(out var value))
{
return value;
}
var formatted = reader.GetString();
var match = _regex.Match(formatted!);
if (!match.Success
|| !long.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture,
out var unixTime))
{
throw new JsonException($"The JSON value '{formatted}' could not be converted to {typeToConvert}.");
}
if (match.Groups.Count < 2 ||
!int.TryParse(match.Groups[3].Value, NumberStyles.Integer, CultureInfo.InvariantCulture,
out var hours) ||
!int.TryParse(match.Groups[4].Value, NumberStyles.Integer, CultureInfo.InvariantCulture,
out var minutes)) return _epoch.AddMilliseconds(unixTime);
var sign = match.Groups[2].Value[0] == '+' ? 1 : -1;
var utcOffset = TimeSpan.FromMinutes(sign * hours * 60 + minutes);
return _epoch.AddMilliseconds(unixTime).ToOffset(utcOffset);
}
private static void WriteCore(Utf8JsonWriter writer, DateTimeOffset value)
{
var unixTime = Convert.ToInt64((value - _epoch).TotalMilliseconds);
var utcOffset = value.Offset;
var stackSize = 64;
while (true)
{
// ReSharper disable once StackAllocInsideLoop
var span = stackSize <= 1024 ? stackalloc byte[stackSize] : new byte[stackSize];
if (!Utf8Formatter.TryFormat(unixTime, span.Slice(7), out var bytesWritten, new StandardFormat('D'))
|| stackSize < 15 + bytesWritten)
{
stackSize *= 2;
continue;
}
// \/Date(
_start.CopyTo(span);
span[7 + bytesWritten] = utcOffset >= TimeSpan.Zero
? 0x2B // +
: 0x2D; // -
var hours = Math.Abs(utcOffset.Hours);
if (hours < 10)
{
span[7 + bytesWritten + 1] = 0x30;
span[7 + bytesWritten + 2] = (byte)(0x30 + hours);
}
else
{
Utf8Formatter.TryFormat(hours, span.Slice(7 + bytesWritten + 1), out _, new StandardFormat('D'));
}
var minutes = Math.Abs(utcOffset.Minutes);
if (minutes < 10)
{
span[7 + bytesWritten + 3] = 0x30;
span[7 + bytesWritten + 4] = (byte)(0x30 + minutes);
}
else
{
Utf8Formatter.TryFormat(minutes, span.Slice(7 + bytesWritten + 3), out _, new StandardFormat('D'));
}
// )\/
_end.CopyTo(span.Slice(7 + bytesWritten + 5));
// avoid to encode the +(\u002B)
var jsonEncodedText = (JsonEncodedText)Activator.CreateInstance(typeof(JsonEncodedText),
BindingFlags.Instance | BindingFlags.NonPublic, null,
new object[] {span.Slice(0, 15 + bytesWritten).ToArray()}, null);
writer.WriteStringValue(jsonEncodedText);
break;
}
}
private class MicrosoftDateTimeConverter : JsonConverter<DateTime>
{
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadCore(ref reader, typeToConvert).DateTime;
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
=> WriteCore(writer, new DateTimeOffset(value, TimeZoneInfo.Local.GetUtcOffset(value)));
}
private class MicrosoftDateTimeOffsetConverter : JsonConverter<DateTimeOffset>
{
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert,
JsonSerializerOptions options)
=> ReadCore(ref reader, typeToConvert);
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
=> WriteCore(writer, value);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment