如何更改数字反序列化中的默认类型

How to Change default type in numeric deserialization

当我将一些 JSON 数据反序列化为 DataSet 时,生成的数据集可能会丢失其列架构。这意味着,当我反序列化一些 JSON 时,它使用 Int64 对象而不是 Int32 填充数据集。我希望它选择 Int32。

我知道,Json.NET 默认情况下将整数值读取为 Int64,因为无法知道该值应该是 Int32 还是 Int64。

JsonSerializerSettings settings = new JsonSerializerSettings()
    {
        Converters = { new PrimitiveJsonConverter() },
    };
DataSet myDataSet = JsonConvert.DeserializeObject<DataSet>(jsonString, settings);

所以我创建了自定义 JsonConverter,以覆盖默认功能。

using DevExpress.XtraPrinting.Native.WebClientUIControl;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization.Formatters;
using System.Text;
using System.Threading.Tasks;
using JsonConverter = Newtonsoft.Json.JsonConverter;

namespace CashlessAdmin.API.Handler
{
    public sealed class PrimitiveJsonConverter : JsonConverter
    {
        readonly JsonSerializer defaultSerializer = new JsonSerializer();

        public override bool CanConvert(Type objectType)
        {
            return objectType.IsIntegerTypes();

        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            switch (reader.TokenType)
            {
                case JsonToken.Integer:
                    if(Convert.ToInt64(reader.Value) < System.Int32.MaxValue)
                    {
                        return Convert.ToInt32(reader.Value);
                    }
                    return reader.Value;
                case JsonToken.Float: // Accepts numbers like 4.00
                case JsonToken.Null:
                    return defaultSerializer.Deserialize(reader, objectType);
                default:
                    throw new JsonSerializationException(string.Format("Token \"{0}\" of type {1} was not a JSON integer", reader.Value, reader.TokenType));
            }
        }

        public override bool CanWrite { get { return false; } }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }

    public static class JsonExtensions
    {
        public static bool IsIntegerTypes(this Type type)
        {
            type = Nullable.GetUnderlyingType(type) ?? type;
            if (type == typeof(long)
                || type == typeof(ulong)
                || type == typeof(int)
                || type == typeof(uint)
                || type == typeof(short)
                || type == typeof(ushort)
                || type == typeof(byte)
                || type == typeof(sbyte)
                || type == typeof(System.Numerics.BigInteger))
                return true;
            return false;
        }
    }
}

但是结果会和之前的情况一样

您的代码不起作用的原因是,在最初推断列类型时,DataTableConverter does not attempt to deserialize the first value encountered for a column. Instead, it simply reads it using JsonReader.Read() and then sets the column type equal to the observed token type, in DataTableConverter.GetColumnDataType()。您的方法 PrimitiveJsonConverter.Read() 此时未被调用。而且,由于 JsonReader.Read() 被设计为 return 一个 long 而不是整数值的 int,数据 table 列类型最终为 long.

您有几个选项可以覆盖 Newtonsoft 的默认行为并获得 Int32 列类型:

  1. 您可以使用 typed DataSet。在这种情况下,列类型将被预定义。

  2. 您可以使用 PreferInt32JsonTextReaderthis answer to Overriding Default Primitive Type Handling in Json.Net(Json.NET 10.0.1 或更高版本)阅读。

  3. 您可以在反序列化后将列转换为 Int32。首先介绍一下扩展方法:

    public static class DataTableExtensions
    {
        public static DataTable RemapInt64ColumnsToInt32(this DataTable table)
        {
            if (table == null)
                throw new ArgumentNullException();
            for (int iCol = 0; iCol < table.Columns.Count; iCol++)
            {
                var col = table.Columns[iCol];
                if (col.DataType == typeof(Int64)
                    && table.AsEnumerable().Where(r => !r.IsNull(col)).Select(r => (Int64)r[col]).All(i => i >= int.MinValue && i <= int.MaxValue))
                {
                    ReplaceColumn(table, col, typeof(Int32), (o, t) => o == null ? null : Convert.ChangeType(o, t, NumberFormatInfo.InvariantInfo));
                }
            }
            return table;
        }
    
        private static DataColumn ReplaceColumn(DataTable table, DataColumn column, Type newColumnType, Func<object, Type, object> map)
        {
            var newValues = table.AsEnumerable()
                .Select(r => r.IsNull(column) ? (object)DBNull.Value : map(r[column], newColumnType))
                .ToList();
    
            var ordinal = column.Ordinal;
            var name = column.ColumnName;
            var @namespace = column.Namespace;
    
            var newColumn = new DataColumn(name, newColumnType);
            newColumn.Namespace = @namespace;
            table.Columns.Remove(column);
            table.Columns.Add(newColumn);
            newColumn.SetOrdinal(ordinal);
    
            for (int i = 0; i < table.Rows.Count; i++)
                if (!(newValues[i] is DBNull))
                    table.Rows[i][newColumn] = newValues[i];
    
            return newColumn;
        }    
    }
    

    然后做:

    var myDataSet = JsonConvert.DeserializeObject<DataSet>(json);
    myDataSet.Tables.Cast<DataTable>().Aggregate((object)null, (o, dt) => dt.RemapInt64ColumnsToInt32());
    

    相关:How To Change DataType of a DataColumn in a DataTable?.

  4. 您可以将自己的 DataTableConverter and modify the logic of DataTableConverter.GetColumnDataType() 版本分叉到 return typeof(Int32) 以获得 JsonToken.Integer 令牌。

    有关所涉及内容的示例,请参阅 to

    由于您的根对象是 DataSet,您还需要分叉您自己的 DataSetConverter and make it use your customized DataTableConverter, as shown in to .

  5. 版本

OP , 它的性能怎么样...?

你得测试看看,看https://ericlippert.com/2012/12/17/performance-rant/

也就是说,一般来说,对于庞大的数据集,您希望避免以某种中间表示形式(例如 JToken 层次结构或单个大型 string)将整个数据集加载到内存中在最终反序列化之前。选项#1、#2 和#4 避免这样做。 #3 确实将一部分数据加载到中间表示中;一些但不是全部 DataTable 列最终被加载然后被替换。因此性能可能还行,但也可能不行——您需要检查一下。

我建议您使用此 DataTableConverter(来自 Newtonsoft.Json 的主 DataTableConverter 的分支)。 这个转换器有两个好处: 1.列的数据类型存储在序列化json中,反序列化后不要改变。 2. 序列化json的大小已经减小,性能已经优化,因为ColumnName不存储每一行​​。

public class DataTableConverter : JsonConverter
{
    /// <summary>
    /// Writes the JSON representation of the object.
    /// </summary>
    /// <param name="writer">The <see cref="JsonWriter"/> to write to.</param>
    /// <param name="value">The value.</param>
    /// <param name="serializer">The calling serializer.</param>
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        DataTable table = (DataTable)value;
        DefaultContractResolver resolver = serializer.ContractResolver as DefaultContractResolver;

        writer.WriteStartObject();

        writer.WritePropertyName("Columns");
        serializer.Serialize(writer, GetColumnDataTypes(table));

        writer.WritePropertyName("Rows");
        writer.WriteStartArray();

        foreach (DataRow row in table.Rows)
        {
            serializer.Serialize(writer, row.ItemArray);
        }

        writer.WriteEndArray();
        writer.WriteEndObject();
    }

    /// <summary>
    /// Reads the JSON representation of the object.
    /// </summary>
    /// <param name="reader">The <see cref="JsonReader"/> to read from.</param>
    /// <param name="objectType">Type of the object.</param>
    /// <param name="existingValue">The existing value of object being read.</param>
    /// <param name="serializer">The calling serializer.</param>
    /// <returns>The object value.</returns>
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
        {
            return null;
        }

        DataTable dataTable = existingValue as DataTable;

        if (dataTable == null)
        {
            // handle typed datasets
            dataTable = (objectType == typeof(DataTable))
                    ? new DataTable()
                    : (DataTable)Activator.CreateInstance(objectType);
        }

        // DataTable is inside a DataSet
        // populate the name from the property name
        if (reader.TokenType == JsonToken.PropertyName)
        {
            dataTable.TableName = (string)reader.Value;

            reader.Read();

            if (reader.TokenType == JsonToken.Null)
            {
                return dataTable;
            }
        }

        if (reader.TokenType == JsonToken.StartObject)
        {
            reader.Read();
            if (reader.TokenType == JsonToken.PropertyName && (string)reader.Value == "Columns")
            {
                reader.Read();

                Dictionary<string, string> columnTypes = new Dictionary<string, string>();
                columnTypes = serializer.Deserialize<Dictionary<string, string>>(reader);

                foreach (KeyValuePair<string, string> column in columnTypes)
                {
                    dataTable.Columns.Add(column.Key, Type.GetType(column.Value));
                }
            }
            reader.Read();
            reader.Read();
        }

        if (reader.TokenType != JsonToken.StartArray)
        {
            throw new JsonSerializationException($"Unexpected JSON token when reading DataTable. Expected StartArray, got {reader.TokenType}.");
        }

        reader.Read();

        while (reader.TokenType != JsonToken.EndArray)
        {
            DataRow dr = dataTable.NewRow();
            dr.ItemArray = serializer.Deserialize<System.Object[]>(reader);
            dataTable.Rows.Add(dr);

            reader.Read();
        }

        reader.Read();

        return dataTable;
    }

    private static Dictionary<string, string> GetColumnDataTypes(DataTable dt)
    {
        Dictionary<string, string> columnTypes = new Dictionary<string, string>();
        foreach (DataColumn column in dt.Columns)
            columnTypes.Add(column.ColumnName, column.DataType.FullName);

        return columnTypes;
    }

    /// <summary>
    /// Determines whether this instance can convert the specified value type.
    /// </summary>
    /// <param name="valueType">Type of the value.</param>
    /// <returns>
    ///     <c>true</c> if this instance can convert the specified value type; otherwise, <c>false</c>.
    /// </returns>
    public override bool CanConvert(Type valueType)
    {
        return typeof(DataTable).IsAssignableFrom(valueType);
    }
}