Почему сериализация DateTimeOffset DataContractJsonSerializer и Json.NET создает разные json?

У меня есть проблема, которую я пытаюсь понять, относительно того, как значения DateTimeOffset сериализуются и десериализуются с использованием DataContractJsonSerializer и Json.NET JsonConvert.

У меня есть следующий класс

[DataContract]
public class TestToSeailize
{
    [DataMember]
    public DateTimeOffset SaveDate { get; set; }
}

Я могу сериализовать это с помощью DataContractJsonSerializer:

TestToSeailize item = new TestToSeailize()
{
    SaveDate = new DateTimeOffset(2020 , 06, 05 , 3 ,0, 0,  TimeSpan.FromHours(5))
};

DataContractJsonSerializer serializer = new DataContractJsonSerializer(item.GetType(), settings);
using (MemoryStream ms = new MemoryStream())
{
    serializer.WriteObject(ms, item);
    var json = Encoding.UTF8.GetString(ms.ToArray()); 
    Console.WriteLine(json);
    return json;
}

это приводит к следующему json {"SaveDate":{"DateTime":"\/Date(1591308000000)\/","OffsetMinutes":300}

и с помощью Json.NET я могу сделать следующее

TestToSeailize item = new TestToSeailize()
{
    SaveDate = new DateTimeOffset(2020, 06, 05, 3, 0, 0, TimeSpan.FromHours(5))
};

string json = JsonConvert.SerializeObject(item);

это приводит к следующему json {"SaveDate":"2020-06-05T03:00:00+05:00"}

Почему они производят разные json? Есть ли способ изменить сериализацию DataContract для создания того же json, что и Json.NET?

Фактическая проблема, которую я пытаюсь решить, заключается в том, чтобы сериализовать данные, сериализованные DataContractJsonSerializer, для десериализации с помощью метода JsonConvert.DeserialzeObject.


person Nick Tucker    schedule 29.06.2020    source источник
comment
Это задокументированный формат для DateTime и DateTimeOffset, см. Даты/время и JSON. Также см. соответствующую документацию Newtonsoft newtonsoft.com/json/help/html/DatesInJSON. хтм.   -  person dbc    schedule 30.06.2020
comment
Установив DataContractJsonSerializerSettings.DateTimeFormat, вы можете получить базовый DateTime в формате ISO 8601 следующим образом: {"SaveDate":{"DateTime":"2020-06-04T22:00:00.00+00:00","OffsetMinutes":300}}. См.: dotnetfiddle.net/tnE2d3. Но, похоже, нет способа сериализовать DateTimeOffset как строку ISO 8601.   -  person dbc    schedule 30.06.2020
comment
Вам может понадобиться добавить суррогатное свойство DateTime для сериализации вашего свойства SaveDate, если вам нужна согласованность между Newtonsoft и DataContractJsonSerializer. Или вы можете написать конвертер на стороне Newtonsoft. Если бы такой конвертер удовлетворил ваши потребности, я, вероятно, мог бы добавить один ответ.   -  person dbc    schedule 30.06.2020


Ответы (1)


JSON, сгенерированный DataContractJsonSerializer для DateTimeOffset и DateTime, соответствует документации. Из Даты/время и JSON:

DateTimeOffset представлен в JSON как сложный тип: {"DateTime":dateTime,"OffsetMinutes":offsetMinutes}. Член offsetMinutes представляет собой смещение местного времени от среднего времени по Гринвичу (GMT), также называемого всемирным скоординированным временем (UTC), связанного с местом интересующего события. Член dateTime представляет экземпляр во времени, когда произошло интересующее событие (опять же, он становится DateTime в JavaScript, когда используется ASP.NET AJAX, и строкой, когда он не используется). При сериализации элемент dateTime всегда сериализуется по Гринвичу. Таким образом, при описании 3:00 по нью-йоркскому времени dateTime имеет временной компонент 8:00, а offsetMinutes равен 300 (минус 300 минут или 5 часов по Гринвичу).

Примечание

Объекты DateTime и DateTimeOffset при сериализации в JSON сохраняют информацию только с точностью до миллисекунды. Значения доли миллисекунды (микро/наносекунды) теряются во время сериализации.

И из Формат передачи даты и времени:

Значения DateTime отображаются в виде строк JSON в форме "/Date(700000+0500)/", где первое число (700000 в приведенном примере) — это количество миллисекунд в часовом поясе по Гринвичу, обычное (не летнее) время с полуночи 1 января 1970 года. Число может быть отрицательным, чтобы представить более ранние времена. Часть, состоящая из +0500 в примере, является необязательной и указывает, что время имеет тип Local, то есть должно быть преобразовано в местный часовой пояс при десериализации. Если он отсутствует, время десериализуется как Utc. Фактическое число (0500 в этом примере) и его знак (+ или -) игнорируются.

Для Newtonsoft см. страницу документации Сериализация дат в JSON для обсуждения как он сериализует даты и время. По умолчанию используются строки формата ISO 8601, но поддерживаются несколько форматов.

Теперь можно настроить формат контракта данных DateTime, установив DataContractJsonSerializerSettings.DateTimeFormat:

var settings = new DataContractJsonSerializerSettings
{
    DateTimeFormat = new DateTimeFormat("yyyy-MM-ddTHH\\:mm\\:ss.ffFFFFFzzz", CultureInfo.InvariantCulture)
    {
    },
};
DataContractJsonSerializer serializer = new DataContractJsonSerializer(item.GetType(), settings);
// Remainder as in your question.

Однако результат для DateTimeOffset выглядит следующим образом:

{"SaveDate":{"DateTime":"2020-06-04T22:00:00.00+00:00","OffsetMinutes":300}}

Это не простая строка, которую вы ищете. Кажется, не существует какого-либо задокументированного способа переопределить формат сериализации для DateTimeOffset. Демонстрационная скрипта №1 здесь.

Поскольку вы написали: Настоящая проблема, которую я пытаюсь решить, заключается в том, чтобы сериализовать данные, сериализованные DataContractJsonSerializer, для десериализации с помощью метода JsonConvert DeserialzeObject, будет намного проще настроить Json.NET для десериализации DataContractJsonSerializer формат. Сначала определите следующий пользовательский JsonConverter:

public class DataContractDateTimeOffsetConverter : JsonConverter
{
    readonly bool canWrite;
    
    public DataContractDateTimeOffsetConverter() : this(true) { }
    public DataContractDateTimeOffsetConverter(bool canWrite) => this.canWrite = canWrite;

    public override bool CanWrite => canWrite;
    public override bool CanConvert(Type objectType) => objectType == typeof(DateTimeOffset) || objectType == typeof(DateTimeOffset?);

    [JsonObject(NamingStrategyType = typeof(DefaultNamingStrategy))] // Ignore camel casing
    class DateTimeOffsetDTO<TOffset> where TOffset : struct, IComparable, IFormattable
    {
        public DateTime DateTime { get; set; }
        public TOffset OffsetMinutes { get; set; }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var input = (DateTimeOffset)value;
        var oldDateFormatHandling = writer.DateFormatHandling;
        var oldDateTimeZoneHandling = writer.DateTimeZoneHandling;
        try
        {
            writer.DateFormatHandling = DateFormatHandling.MicrosoftDateFormat;
            writer.DateTimeZoneHandling = DateTimeZoneHandling.Utc;
            var offsetMinutes = input.Offset.TotalMinutes;
            var offsetMinutesInt = checked((int)offsetMinutes);
            var dateTime = input.DateTime.AddMinutes(-input.Offset.TotalMinutes);
            if (offsetMinutesInt == offsetMinutes) // An integer number of mintues
                serializer.Serialize(writer, new DateTimeOffsetDTO<int> { DateTime = dateTime, OffsetMinutes = offsetMinutesInt });
            else
                serializer.Serialize(writer, new DateTimeOffsetDTO<double> { DateTime = dateTime, OffsetMinutes = offsetMinutes });
        }
        finally
        {
            writer.DateFormatHandling = oldDateFormatHandling;
            writer.DateTimeZoneHandling = oldDateTimeZoneHandling;
        }
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        switch (reader.MoveToContentAndAssert().TokenType)
        {
            // note that if there is a possibility of getting ISO 8601 strings for DateTimeOffset as well as complex objects, you may need to configure
            // JsonSerializerSettings.DateParseHandling = DateParseHandling.None or DateParseHandling.DateTimeOffset at a higher code level to 
            // avoid premature deserialization as DateTime by JsonTextReader.
            case JsonToken.String:
            case JsonToken.Date:
                return (DateTimeOffset)JToken.Load(reader);
                
            case JsonToken.StartObject:
                var old = reader.DateTimeZoneHandling;
                try
                {
                    reader.DateTimeZoneHandling = DateTimeZoneHandling.Utc;
                    var dto = serializer.Deserialize<DateTimeOffsetDTO<double>>(reader);
                    var result = new DateTimeOffset(new DateTime(dto.DateTime.AddMinutes(dto.OffsetMinutes).Ticks, DateTimeKind.Unspecified), 
                                                    TimeSpan.FromMinutes(dto.OffsetMinutes));
                    return result;
                }
                finally
                {
                    reader.DateTimeZoneHandling = old;
                }
                
            case JsonToken.Null:
                return null;    
                
            default:
                throw new JsonSerializationException(); // Unknown token
        }
    }
}

public static partial class JsonExtensions
{
    public static JsonReader MoveToContentAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (reader.TokenType == JsonToken.None)       // Skip past beginning of stream.
            reader.ReadAndAssert();
        while (reader.TokenType == JsonToken.Comment) // Skip past comments.
            reader.ReadAndAssert();
        return reader;
    }

    public static JsonReader ReadAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (!reader.Read())
            throw new JsonReaderException("Unexpected end of JSON stream.");
        return reader;
    }
}

Теперь вы можете десериализовать JSON, сгенерированный DataContractJsonSerializer, добавив преобразователь в JsonSerializerSettings.Converters:

var settings = new JsonSerializerSettings
{
    Converters = { new DataContractDateTimeOffsetConverter(true) },
};

var item = JsonConvert.DeserializeObject<TestToSeailize>(json, settings);

Примечания:

  • Если вы не хотите сериализоваться в формате DataContractJsonSerializer, передайте canWrite : false конструктору преобразователя.

  • Если существует возможность получения строк ISO 8601, а также сложных объектов для значений DateTimeOffset, вам может потребоваться настроить JsonSerializerSettings.DateParseHandling = DateParseHandling.None или DateParseHandling.DateTimeOffset на более высоком уровне кода, чтобы избежать преждевременной десериализации строк ISO 8601 как DateTime объектов с помощью JsonTextReader.

Демонстрационная скрипта № 2 здесь.

person dbc    schedule 01.07.2020