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

Posted

技术标签:

【中文标题】为啥 DateTimeOffset 的 DataContractJsonSerializer 和 Json.NET 序列化会产生不同的 json?【英文标题】:Why do DataContractJsonSerializer and Json.NET serialization of DateTimeOffset produce different json?为什么 DateTimeOffset 的 DataContractJsonSerializer 和 Json.NET 序列化会产生不同的 json? 【发布时间】:2020-10-19 08:59:31 【问题描述】:

关于使用 DataContractJsonSerializer 和 Json.NET 的 JsonConvert 对 DateTimeOffset 值进行序列化和反序列化的方式,我想了解一个问题。

我有以下课程

[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 方法反序列化。

【问题讨论】:

这是DateTimeDateTimeOffset 的文档格式,请参阅Dates/Times and JSON。另请参阅相关的 Newtonsoft 文档newtonsoft.com/json/help/html/DatesInJSON.htm。 通过设置DataContractJsonSerializerSettings.DateTimeFormat,您可以获得ISO 8601格式的底层DateTime,如下所示:"SaveDate":"DateTime":"2020-06-04T22:00:00.00+00:00","OffsetMinutes":300。见:dotnetfiddle.net/tnE2d3。但似乎没有办法将DateTimeOffset 序列化为 ISO 8601 字符串。 如果您需要 Newtonsoft 和 DataContractJsonSerializer 之间的一致性,您可能需要添加一个代理 DateTime 属性来序列化您的 SaveDate 属性。或者你可以在 Newtonsoft 端写一个转换器。如果这样的转换器可以满足您的需求,我可能会添加一个答案。 【参考方案1】:

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

DateTimeOffset 在 JSON 中表示为复杂类型:"DateTime":dateTime,"OffsetMinutes":offsetMinutesoffsetMinutes 成员是与感兴趣事件的位置相关联的格林威治标准时间 (GMT)(现在也称为协调世界时 (UTC))的本地时间偏移量。 dateTime 成员表示感兴趣的事件发生时的实例(同样,在使用 ASP.NET AJAX 时,它在 javascript 中变为 DateTime,在不使用时变为字符串)。在序列化时,dateTime 成员始终在 GMT 中序列化。因此,如果描述纽约时间凌晨 3:00,dateTime 的时间分量为 8:00 AM,offsetMinutes 为 300(从 GMT 减去 300 分钟或 5 小时)。

注意

DateTime 和 DateTimeOffset 对象在序列化为 JSON 时,只保留毫秒精度的信息。序列化过程中会丢失亚毫秒值(微/纳秒)。

来自DateTime Wire Format:

DateTime 值显示为"/Date(700000+0500)/" 形式的 JSON 字符串,其中第一个数字(在提供的示例中为 700000)是 GMT 时区的毫秒数,自午夜以来的常规(非夏令时)时间, 1970 年 1 月 1 日。该数字可能为负数以表示较早的时间。示例中由“+0500”组成的部分是可选的,表示时间是本地类型的——也就是说,应该在反序列化时转换为本地时区。如果不存在,则将时间反序列化为 Utc。实际数字(本例中为“0500”)及其符号(+ 或 -)将被忽略。

对于 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 的序列化格式。演示小提琴 #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 以避免过早将 ISO 8601 字符串反序列化为DateTimeJsonTextReader 的对象。

演示小提琴#2 here.

【讨论】:

以上是关于为啥 DateTimeOffset 的 DataContractJsonSerializer 和 Json.NET 序列化会产生不同的 json?的主要内容,如果未能解决你的问题,请参考以下文章

选择 SQL Server DatetimeOffset 作为 .Net DateTimeOffset.Ticks

“DateTime”和“DateTimeOffset”之间的区别[重复]

模棱两可的 DateTimeOffset 示例

如何将 C# datetimeOffset 转换为指定格式

你啥时候更喜欢 DateTime 而不是 DateTimeOffset

datetimeoffset 休眠映射