如何在 Delphi 中扩展 JSON $date,或者它是关于 System.JSON 中时区和 UTC 的错误?

How to do extended JSON $date right in Delphi, or is it a bug about timezones and UTC in System.JSON?

我正在尝试使用 TJSONObjectBuilder...AddPairs() 解析扩展 JSON。我的 JSON 包含一个 $date(我需要它在 Utc 中用于 MongoDB)。但是不知何故,时区被打破了,不管我的输入是否已经是 Utc。

Input : {"Zulu":{"$date":"2019-01-01T00:00:00.000Z"},"Utc+1":{"$date":"2019-01-01T01:00:00.000+01:00"}}
Output: {"Zulu":{"$date":"2019-01-01T01:00:00.000Z"},"Utc+1":{"$date":"2019-01-01T01:00:00.000Z"}}
                                      ^                                            ^

没有 TJsonDateTimeZoneHandling.Utc 它是正确的,但这对我没有帮助,因为我需要 Utc 格式的结果:

Output: {"Zulu":{"$date":"2019-01-01T01:00:00.000+01:00"},"Utc+1":{"$date":"2019-01-01T01:00:00.000+01:00"}}

这是我展示它的最少代码:

program SystemJsonDateTest;
{$APPTYPE CONSOLE}
uses
  System.Classes, System.JSON.Types, System.JSON.Writers, System.JSON.Builders;
var
  StringWriter: TStringWriter;
  JsonWriter: TJsonTextWriter;
  Builder: TJSONObjectBuilder;
begin
  StringWriter:= TStringWriter.Create;

  JsonWriter:= TJsonTextWriter.Create(StringWriter);
  JsonWriter.ExtendedJsonMode:= TJsonExtendedJsonMode.StrictMode;
  JsonWriter.DateTimeZoneHandling:= TJsonDateTimeZoneHandling.Utc;

  TJSONObjectBuilder.Create(JsonWriter)
    .BeginObject
      .AddPairs('{"Zulu":{"$date":"2019-01-01T00:00:00.000Z"},'
      + '"Utc+1":{"$date":"2019-01-01T01:00:00.000+01:00"},'
      + '"Unix":{"$date":1546300800000}}')
    .EndObject
    .Free;

  JsonWriter.Free;
  WriteLn(StringWriter.ToString);
  StringWriter.Free;
  ReadLn;
end.

背景: 我正在使用 TMongoDocument.AsJSON,发现了这种行为并尝试用最少的代码重现它并且没有任何参考 MongoDB 组件。如果我做了一些奇怪的事情或者演示可以更简化,请评论...

在那个 MongoDocument 中,使用 TBsonWriter 代替,但它显示了同样的问题:

Stream:= TFileStream.Create('file.bson', fmCreate);
BsonWriter:= TBsonWriter.Create(Stream);
TJSONObjectBuilder.Create(BsonWriter).BeginObject.AddPairs(//see above

我知道,这是很多文字 - 如果您忘记了问题,它在标题中;)

MongoDB 客户端可能支持 JSON 输入中 Date 字段的“$date”扩展语法中的区域(即使 Delphi 客户端似乎忽略了它),但 MongoDB 服务器不会处理其 BSON 存储中的区域。

事实上,Date 值的 reference documentation states 存储为 UTC - 它们甚至在 BSON 格式中被称为 UTC Date,并存储为 Unix 毫秒数的 Int64:

BSON Date is a 64-bit integer that represents the number of milliseconds since the Unix epoch (Jan 1, 1970). This results in a representable date range of about 290 million years into the past and future.

因此,您的 "Utc+1""Zulu" 字段将包含完全相同的 UTC 时间戳,即使在客户端库正确转换时区之后也是如此。

所以您最好只将 UTC 日期发送到 MongoDB,并在客户端进行转换。即使转换正确,在所有情况下您都会丢失区域信息,因为它将存储为 UTC。并且不要使用 ISO-8601 文本进行传输,而只是 UnixTime 值,作为整数:

function DateTimeToUnixMSTime(const AValue: TDateTime): Int64;
begin
  result := Round((AValue - UnixDateDelta) * MSecsPerDay);
end;

顺便说一句,最好只在任何类型的数据库中使用 UTC 日期,然后使用即时转换到 display/reporting 上的当前本地用户,并将本地区域存储在一个单独的区域中字段,如果确实需要,可以作为文本标识符,也可以作为以天为单位的浮点偏差(可能更方便 - 请注意区域偏差不是必需的整数,例如阿富汗)。

是的,是bug,10.3已经修复

在单元System.JSON.Builders中,TJSONCollectionBuilder.TBaseCollection.WriteJSON()创建了一个默认DateTimeZoneHandling=Local的TJsonTextReader,这意味着任何DateTimes都被转换为本地。

但在 System.JSON.Writers、TJsonTextWriter.WriteValue(Value: TDateTime) 中,当 DateTimeZoneHandling=Utc 时,不能包含时区的 DateTime 值被预期并解释为 Utc。

因此,TJSONCollectionBuilder 需要一个 DateTimeZoneHandling=Local 的 Writer,这使得无法以正确的 Utc 获取输出。

...调试之后,我知道 google 的用途: http://docwiki.embarcadero.com/RADStudio/Rio/en/New_features_and_customer_reported_issues_fixed_in_RAD_Studio_10.3

  • TMongoDocument 和 TJSONCollectionBuilder 错误地解析 ISO 日期 数据,Data\FireDAC,RTL,RTL\Delphi RSP-17046
  • [FireDAC,MongoDB] [TJSONCollectionBuilder.TBaseCollection.WriteJSON] 日期总是 如果值在文本模式数据中更新,则转换为本地时区, Data\FireDAC、RTL\Delphi RSP-20571