Last active
February 22, 2021 16:49
-
-
Save akunzai/7f4eec3ac912f6bb540d6d10d7d6bd34 to your computer and use it in GitHub Desktop.
System.Text.Json deserialize legacy JSON date workaround
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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