PAYATHON 2026

首行是 NULL 时如何阻止 JSON 反序列化转换 DataSet 日期值

排障赵师傅

结论

要让 DATER 保持字符串类型,并完整保留 JSON 中的 "2026-02-16T11:17:43.803",需要设置:

DateParseHandling = DateParseHandling.None

DateFormatHandling.IsoDateFormat 控制的是日期序列化后的输出格式,无法阻止反序列化器将 ISO 8601 字符串解析为 DateTime。

如果旧程序要求 DATER 始终为 DateTime,不要让 DataSet 根据第一行数据推断列类型。可以先明确数据结构,再创建类型固定的 DataColumn。

保留原始日期字符串

设置方式如下:

using Newtonsoft.Json;
using System.Data;

string sTest =
    "{\"Table1\":[{\"DATER\":null},{\"DATER\":\"2026-02-16T11:17:43.803\"}]}";

var settings = new JsonSerializerSettings
{
    DateParseHandling = DateParseHandling.None
};

DataSet moDataSet =
    JsonConvert.DeserializeObject<DataSet>(sTest, settings);

string columnType =
    moDataSet.Tables[0].Columns["DATER"].DataType.Name;

object firstValue =
    moDataSet.Tables[0].Rows[0]["DATER"];

object secondValue =
    moDataSet.Tables[0].Rows[1]["DATER"];

预期结果为:

columnType = "String"
firstValue = DBNull.Value
secondValue = "2026-02-16T11:17:43.803"

设置 DateParseHandling.None 后,JSON.NET 会把日期文本作为普通字符串读取,不会先转成 DateTime,再根据当前区域设置转换成 "02/16/2026 11:17:43" 这样的字符串。

为什么首行是 null 会改变结果

JSON 没有 DateTime 数据类型。下面这个值在 JSON 中只是字符串:

"2026-02-16T11:17:43.803"

JSON.NET 的读取器默认会识别符合日期格式的字符串,并将其转换为 DateTime token。DataSet 转换器则需要根据数据内容推断 DataColumn.DataType。

第一行有日期值时,转换器可以将该列推断为 DateTime。第一行是 null 时,转换器缺少判断列类型的信息,后续可能会把该列创建为 String。如果后面的 ISO 日期已经被读取器解析为 DateTime,写入字符串列时还会发生一次从 DateTime 到字符串的转换。转换结果可能受到当前区域设置影响,毫秒部分也可能丢失。

这里涉及两个处理环节:

  1. JsonTextReader 是否将 ISO 字符串解析为 DateTime。
  2. DataSet 转换器如何推断 DataColumn.DataType。

DateParseHandling.None 作用于第一个环节,用来避免原始字符串被提前解析。

如果旧程序要求该列始终为 DateTime

保留原始字符串和让列类型始终为 DateTime 是两项不同的要求。DateTime 值本身不保存 "2026-02-16T11:17:43.803" 这样的显示格式,只有将日期转换为字符串时才需要指定格式。

如果旧代码需要直接执行:

DateTime value = (DateTime)row["DATER"];

应显式定义列类型,不再让 DataSet 根据第一行数据推断。可以先将 JSON 反序列化为强类型对象,再创建 DataTable:

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Data;

public sealed class ApiResult
{
    public List<ApiRow> Table1 { get; set; }
}

public sealed class ApiRow
{
    public DateTime? DATER { get; set; }
}

转换代码如下:

string json =
    "{\"Table1\":[{\"DATER\":null},{\"DATER\":\"2026-02-16T11:17:43.803\"}]}";

ApiResult result = JsonConvert.DeserializeObject<ApiResult>(json);

var table = new DataTable("Table1");

DataColumn daterColumn = table.Columns.Add("DATER", typeof(DateTime));
daterColumn.AllowDBNull = true;

foreach (ApiRow item in result.Table1)
{
    DataRow row = table.NewRow();

    row["DATER"] = item.DATER.HasValue
        ? (object)item.DATER.Value
        : DBNull.Value;

    table.Rows.Add(row);
}

var dataSet = new DataSet();
dataSet.Tables.Add(table);

无论第一行是否为 null,下面的判断都会成立:

dataSet.Tables[0].Columns["DATER"].DataType == typeof(DateTime)

空值使用 DBNull.Value,非空值则保存为实际的 DateTime。

再次输出 ISO 格式时,需要明确指定格式:

DateTime value =
    (DateTime)dataSet.Tables[0].Rows[1]["DATER"];

string text = value.ToString(
    "yyyy-MM-dd'T'HH:mm:ss.fff",
    System.Globalization.CultureInfo.InvariantCulture);

结果为:

2026-02-16T11:17:43.803

注意事项

  • DateFormatHandling 不能替代 DateParseHandling。前者控制日期的序列化表示,后者决定读取 JSON 时是否自动识别日期。
  • 如果 REST API 可能返回带时区的值,例如 Z 或 +08:00,可根据接口语义使用 DateTimeOffset?,以免丢失偏移量信息。
  • 示例 2 中仍然只有一个名为 Table1 的表,因此应通过 Tables[0].Rows[1] 查询非空值,不能使用 Tables[1].Rows[0]。
  • 只需保留原始文本时,使用 DateParseHandling.None。需要兼容旧程序中的 DateTime 列时,应采用固定 schema 或强类型中间对象。

备注:内容仅供参考。