为什么 DataContractJsonSerializer 和 DateTimeOffset 的 Json.NET 序列化产生不同的 json?

Why do DataContractJsonSerializer and Json.NET serialization of DateTimeOffset produce different json?

我有一个问题,我试图了解有关使用 DataContractJsonSerializer 和 Json.NET 的 JsonConvert 序列化和反序列化 DateTimeOffset 值的方式。

我有以下class

[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.NET 相同的 json 产品?

我试图解决的实际问题是让 DataContractJsonSerializer 序列化的数据通过 JsonConvert.DeserialzeObject 方法反序列化。

DataContractJsonSerializerDateTimeOffsetDateTime 生成的 JSON 已记录在案。来自 Dates/Times and JSON:

DateTimeOffset is represented in JSON as a complex type: {"DateTime":dateTime,"OffsetMinutes":offsetMinutes}. The offsetMinutes member is the local time offset from Greenwich Mean Time (GMT), also now referred to as Coordinated Universal Time (UTC), associated with the location of the event of interest. The dateTime member represents the instance in time when the event of interest occurred (again, it becomes a DateTime in JavaScript when ASP.NET AJAX is in use and a string when it is not). On serialization, the dateTime member is always serialized in GMT. So, if describing 3:00 AM New York time, dateTime has a time component of 8:00 AM and offsetMinutes are 300 (minus 300 minutes or 5 hours from GMT).

Note

DateTime and DateTimeOffset objects, when serialized to JSON, only preserve information to millisecond precision. Sub-millisecond values (micro/nanoseconds) are lost during serialization.

来自DateTime Wire Format

DateTime values appear as JSON strings in the form of "/Date(700000+0500)/", where the first number (700000 in the example provided) is the number of milliseconds in the GMT time zone, regular (non-daylight savings) time since midnight, January 1, 1970. The number may be negative to represent earlier times. The part that consists of "+0500" in the example is optional and indicates that the time is of the Local kind - that is, should be converted to the local time zone on deserialization. If it is absent, the time is deserialized as Utc. The actual number ("0500" in this example) and its sign (+ or -) are ignored.

对于 Newtonsoft,请参阅文档页面 Serializing Dates in JSON 以讨论它如何序列化日期和时间。默认使用 ISO 8601 格式字符串,但支持多种格式。

现在,可以通过设置 DataContractJsonSerializerSettings.DateTimeFormat:

来自定义数据合同 DateTime 格式
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 的序列化格式。演示 fiddle #1 here.

既然你写了,我想解决的实际问题是让 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;
    }
}

现在您可以通过将转换器添加到 JsonSerializerSettings.Converters 来反序列化由 DataContractJsonSerializer 生成的 JSON:

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

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

备注:

  • 如果不想以 DataContractJsonSerializer 格式序列化,将 canWrite : false 传递给转换器的构造函数。

  • 如果有可能获取 ISO 8601 字符串以及 DateTimeOffset 值的复杂对象,您可能需要配置 JsonSerializerSettings.DateParseHandling = DateParseHandling.NoneDateParseHandling.DateTimeOffset更高的代码级别以避免 JsonTextReader.

    将 ISO 8601 字符串过早反序列化为 DateTime 对象

演示 fiddle #2 here.