一聚教程网:一个值得你收藏的教程网站

热门教程

.NET 中的序列化和反序列化问题示例完整指南

时间:2026-08-27 11:44:01 编辑:袖梨 来源:一聚教程网

平时做技术实践时,很多问题不是概念不会,而是细节没串起来。拿“.NET 中的序列化和反序列化问题示例”来说,它看着像小点,放到项目里常会牵出环境、配置、兼容性和维护成本。下面按实际采用顺序,把思路、关键写法和容易踩坑的地方讲清楚,便于大家直接对照操作。

.NET 中的序列化和反序列化

从实现思路看,在 ASP.NET Core 应用里,框架会屏蔽了很多实现序列化和反序列化的细节,我们只需定义参数模型,ASP.NET Core 会自动将 http 请求的 Body 反序列化为模型对象。但是日常开发中我们会对序列化和反序列化做许多定制设置,比如忽略值为 null 的字段、时间格式处理、忽略大小写、字段类型转换等各种情况。所以笔者单独采用一章讲解序列化框架的采用以及如何进行定制,深入了解 .NET 中序列化和反序列化机制。

结合项目来看,System.Text.Json 是 .NET 框架自带的序列化框架,轻松易用同时且性能也很出色,采用 System.Text.Json 反序列化字符串为对象是很轻松的,示比如下所示:

// 自定义序列化配置
static JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions()
{
    PropertyNameCaseInsensitive = true,
    WriteIndented = true
};
public static void Main()
{
    const string json =
        """
            {
                "Name": "工良"
            }
            """;
    var obj = JsonSerializer.Deserialize<Model>(json, jsonSerializerOptions);
}
public class Model
{
    public string Name { get; set; }
}

落到代码里,JsonSerializerOptions 的属性定义了如何序列化和反序列化,其常用属性如下所示:

属性类型说明
AllowTrailingCommasbool忽略 JSON 中多余的逗号
ConvertersIList<JsonConverter>转换器列表
DefaultBufferSizeint默认缓冲区大小
DefaultIgnoreConditionJsonIgnoreCondition字段/属性的值为默认值时,是否忽略
DictionaryKeyPolicyJsonNamingPolicy字典 Key 重命名规则,如首字母生成小写
IgnoreNullValuesbool忽略 JSON 中值为 null 的字段/属性
IgnoreReadOnlyFieldsbool忽略只读字段
IgnoreReadOnlyPropertiesbool忽略只读属性
IncludeFieldsbool是否处理字段,默认只处理属性
MaxDepthint最大嵌套深度,默认最大深度为 64
NumberHandlingJsonNumberHandling如何处理数字类型
PropertyNameCaseInsensitivebool忽略大小写
PropertyNamingPolicyJsonNamingPolicy重命名规则,如首字母生成小写
ReadCommentHandlingJsonCommentHandling处理注释
WriteIndentedbool序列化时格式化 JSON,如换行、空格、缩进

在这个场景下,下面笔者将会列举一些常用的定制场景和编码方法,为了避免混肴,在本章中所指的 “字段” 或 “属性”,等同于类型的“字段和属性”。

编写类型转换器

理解这一步时,类型转换器的作用是当 json 对象字段和模型类字段类型不一致时,能够自动转换对应的类型,下面笔者介绍常用的几种类型转换器。

枚举转换器

.NET 是如何序列化枚举

落到代码里,编写 WebAPI 的模型类时常常会用到枚举,枚举类型默认会以数值的形式输出到 json 中。

C# 代码示比如下所示:

// 枚举
public enum NetworkType
    {
        Unknown = 0,
        IPV4 = 1,
        IPV6 = 2
    }
// 类型
public class Model
    {
        public string Name { get; set; }
        public NetworkType Netwotk1 { get; set; }
        public NetworkType? Netwotk2 { get; set; }
    }
var model = new Model
        {
            Name = "工良",
            Netwotk1 = NetworkType.IPV4,
            Netwotk2 = NetworkType.IPV6
        }

我们序列化对象时,会得到这样的结果:

{
    "Name": "工良",
    "Netwotk1": 1,
    "Netwotk2": 2
}

落到代码里,但是这样会在阅读上带来难题,数字记忆比较困难,同时且后期需扩展枚举字段时,可能会导致对应数值的变化,那么已经对接的代码都需修改,如果枚举涉及的范围比较广,那么要做出修改就会变得十分困难。

在这个场景下,例如突然出现了一个 IPV5,那么我们除了改代码,可能还要修改以及对接的其它应用。

public enum NetworkType
{
        Unknown = 0,
        IPV4 = 1,
        IPV5 = 2,
        IPV6 = 3
}

结合项目来看,所以,我们需一种方法,能够让枚举序列化后采用对应的名称表示,以及能够采用这个字符串转化为对应的枚举类型,后期需扩展或中间插入时,对以前的代码和数据库完全没有影响。

比如反序列化时,得到的是这样的 json:

"Netwotk1": "IPV4"
"Netwotk2": "IPV6"

落到代码里,即使后来中间插入一个 IPV5,生成新的字符串即可,完全不需重新排序枚举值。

"Netwotk1": "IPV4"
"Netwotk2": "IPV6"
"Netwotk3": "IPV5"

理解这一步时,在 C# 模型类中采用枚举而 json 中采用字符串,要实现这种形式的枚举转换,有两种方法。

  • 落到代码里,在模型类的枚举字段或属性上放置一个特性注解,序列化反序列化时从这个特性注解中拿到转换器。
  • 实际处理时,采用 JsonSerializerOptions 添加转换器,在反序列化或序列化时传递自定义设置。

结合项目来看,无论哪种方法,我们都需实现一个转换器,能够将模型类中的枚举采用对应的名称序列化到 json 中。在实现自定义转换器示例之前,我们来了解相关的一些知识。

自定义转换器需继承 JsonConverterJsonConverter<T>,当反序列化 json 的字段或序列化对象的字段属性时,框架会自动调用转换器。

JsonConverter<T> 为例,里面有好几个抽象接口,我们一般只需实现转换器的两个抽象接口即可:

// json 值 => 对象字段
public abstract T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options);
// 对象字段 => json 值
public abstract void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options);

在这个场景下,不过我们一定需留意 C# 中的可空类型,比如 NetworkTypeNetworkType? 实际上是两种类型,可空类型本质是采用 Nullable<T> 包装的类型。

Nullable<T> 的定义如下所示:

public struct Nullable<T> where T : struct

另外 Nullable<T> 实现了和 T 类型的隐式和显式转换重载,所以我们在采用可空类型时,可能不太容易感受出 Nullable<T> 和 T 区别,比如能够在采用可空类型 T? 时,直接将 Nullable<T>T 类型隐式和显式转换,如:

Nullable<int> value = 100

但是在采用反射时,由于 TT? 是两种不同的类型,所以我们编写转换器时必须留意到这种区别,否则会出现错误。

实现枚举转换器

本节示例在 Demo4.Console 中。

编写一个枚举字符串转换器代码示比如下所示:

public class EnumStringConverter<TEnum> : JsonConverter<TEnum>
{
    private readonly bool _isNullable;
    public EnumStringConverter(bool isNullType)
    {
        _isNullable = isNullType;
    }
    // 判断当前类型是否可以使用该转换器转换
    public override bool CanConvert(Type objectType) => EnumStringConverterFactory.IsEnum(objectType);
    // 从 json 中读取数据
    // JSON => 值
    // typeToConvert: 模型类属性/字段的类型
    public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        // 读取 json
        var value = reader.GetString();
        if (value == null)
        {
            if (_isNullable) return default;
            throw new ArgumentNullException(nameof(value));
        }
        // 是否为可空类型
        var sourceType = EnumStringConverterFactory.GetSourceType(typeof(TEnum));
        if (Enum.TryParse(sourceType, value.ToString(), out var result))
        {
            return (TEnum)result!;
        }
        throw new InvalidOperationException($"{value} 值不在枚举 {typeof(TEnum).Name} 范围中");
    }
    // 值 => JSON
    public override void Write(Utf8JsonWriter writer, TEnum? value, JsonSerializerOptions options)
    {
        if (value == null) writer.WriteNullValue();
        else writer.WriteStringValue(Enum.GetName(value.GetType(), value));
    }
}

在这个场景下,由于 Utf8JsonReader 日常出行的机会不多,所以读者可能不太了解,在本章的末尾,笔者会轻松介绍。

从实现思路看,一般情况下,我们不会直接采用 EnumStringConverter,为了能够适应所有枚举类型,还需编写一个枚举转换工厂,借助工厂模式判断输入类型之后,再新建对应的转换器。

public class EnumStringConverterFactory : JsonConverterFactory
{
    // 获取需要转换的类型
    public static bool IsEnum(Type objectType)
    {
        if (objectType.IsEnum) return true;
        var sourceType = Nullable.GetUnderlyingType(objectType);
        return sourceType is not null && sourceType.IsEnum;
    }
    // 如果类型是可空类型,则获取原类型
    public static Type GetSourceType(Type typeToConvert)
    {
        if (typeToConvert.IsEnum) return typeToConvert;
        return Nullable.GetUnderlyingType(typeToConvert);
    }
    // 判断该类型是否属于枚举
    public override bool CanConvert(Type typeToConvert) => IsEnum(typeToConvert);
    // 为该字段创建一个对应的类型转换器
    public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        var sourceType = GetSourceType(typeToConvert);
        var converter = typeof(EnumStringConverter<>).MakeGenericType(typeToConvert);
        return (JsonConverter)Activator.CreateInstance(converter, new object[] { sourceType != typeToConvert });
    }
}

结合项目来看,System.Text.Json 处理一个字段时,会调用 EnumStringConverterFactory 的 CanConvert 方法,如果得到 true,则会调用 EnumStringConverterFactory 的 CreateConverter 方法创转换器,最后调用转换器处理字段,这样一来,我们能够借助泛型类 EnumStringConverter<TEnum> 处理各种枚举。

随后定义特性注解,能够将模型类的属性字段绑定到一个转换器上。

    [AttributeUsage(AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
    public class EnumConverterAttribute : JsonConverterAttribute
    {
        public override JsonConverter CreateConverter(Type typeToConvert)
        {
            return new EnumStringConverterFactory();
        }
    }

如何采用类型转换器

采用自定义类型转换器有三种方法。

方法一,在枚举字段中采用自定义特性:

    public class Model
    {
        public string Name { get; set; }
        [EnumConverter]
        public NetworkType Netwotk1 { get; set; }
        [EnumConverter]
        public NetworkType? Netwotk2 { get; set; }
    }

方法二,采用 JsonConverter 特性。

public class Model
{
    public string Name { get; set; }
    [JsonConverter(typeof(EnumConverter))]
    public NetworkType Netwotk1 { get; set; }
    [JsonConverter(typeof(EnumConverter))]
    public NetworkType? Netwotk2 { get; set; }
}

方法三,在设置中添加转换器。

        jsonSerializerOptions.Converters.Add(new EnumStringConverterFactory());
        var obj = JsonSerializer.Deserialize<Model>(json, jsonSerializerOptions);

在模型类中采用转换器特性之后,我们能够借助字符串反序列化为枚举类型:

        const string json =
            """
            {
                "Name": "工良",
                "Netwotk1": "IPV4",
                "Netwotk2": "IPV6"
            }
            """;
        var obj = JsonSerializer.Deserialize<Model>(json, jsonSerializerOptions);

采用官方的转换器

在这个场景下,System.Text.Json 中已经实现了很多转换器,能够在官方源码的 System/Text/Json/Serialization/Converters/Value 下找到所有自带的转换器,其中官方实现的枚举字符串转换器叫 JsonStringEnumConverter,采用方法跟我们的自定义转换器一致。

这里能够采用官方的 JsonStringEnumConverter 转换器替代 EnumStringConverter<TEnum>

    public class Model
    {
        public string Name { get; set; }
        public NetworkType Netwotk1 { get; set; }
        public NetworkType? Netwotk2 { get; set; }
    }
        JsonSerializerOptions jsonSerializerOptions = new();
        jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
        const string json =
            """
            {
                "Name": "工良",
                "Netwotk1": "IPV4",
                "Netwotk2": "IPV6"
            }
            """;
        var obj = JsonSerializer.Deserialize<Model>(json, jsonSerializerOptions);

字符串和值类型转换

结合项目来看,很多情况下,会在模型类下采用数值类型,序列化到 json 时采用字符串。比如对应浮点型的数值,为了保证其准确性,我们会采用字符串形式保存到 json 中,这样能够避免传输时对浮点型处理而丢失其准确性。又比如前端处理超过 16 位数值时,数字会丢失精确度,16位数字存储毫秒格式的时间戳足够了,很多时候我们会采用分布式 id,雪花算法有很多种,其生成的 id 往往会超过 16 位。

JS 中处理超过 16 位数字时,会出现很精确度丢失的问题:

console.log(11111111111111111);
输出: 11111111111111112
console.log(111111111111111111);
输出: 111111111111111100

有个最轻松的方法是在 JsonSerializerOptions 中将所有数值字段转换为字符串:

        new JsonSerializerOptions
        {
            NumberHandling = JsonNumberHandling.AllowReadingFromString
        };

从实现思路看,但是这样会导致所有值类型字段序列化为 json 时变成字符串,如果只需处理几个字段而不是处理所有字段,那就需我们自己编写类型转换器了。

在这个场景下,要实现字符串转数值,需考虑很多种数值类型,如 byte、int、double、long 等,从值类型转换为字符串是很轻松的,但是要实现一个字符串转任意类型值类型,那就很麻烦,这也是我们编写转换器的重点。

编写 json 字符串和模型类值类型转换器的代码示比如下所示:

public class StringNumberConverter<T> : JsonConverter<T>
{
    private static readonly TypeCode typeCode = Type.GetTypeCode(typeof(T));
    // 从 json 中读取字符串,转换为对应的值类型
    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        switch (reader.TokenType)
        {
            case JsonTokenType.Number:
                if (typeCode == TypeCode.Int32)
                {
                    if (reader.TryGetInt32(out var value))
                    {
                        return Unsafe.As<int, T>(ref value);
                    }
                }
                if (typeCode == TypeCode.Int64)
                {
                    if (reader.TryGetInt64(out var value))
                    {
                        return Unsafe.As<long, T>(ref value);
                    }
                }
                if (typeCode == TypeCode.Decimal)
                {
                    if (reader.TryGetDecimal(out var value))
                    {
                        return Unsafe.As<decimal, T>(ref value);
                    }
                }
                if (typeCode == TypeCode.Double)
                {
                    if (reader.TryGetDouble(out var value))
                    {
                        return Unsafe.As<double, T>(ref value);
                    }
                }
                if (typeCode == TypeCode.Single)
                {
                    if (reader.TryGetSingle(out var value))
                    {
                        return Unsafe.As<float, T>(ref value);
                    }
                }
                if (typeCode == TypeCode.Byte)
                {
                    if (reader.TryGetByte(out var value))
                    {
                        return Unsafe.As<byte, T>(ref value);
                    }
                }
                if (typeCode == TypeCode.SByte)
                {
                    if (reader.TryGetSByte(out var value))
                    {
                        return Unsafe.As<sbyte, T>(ref value);
                    }
                }
                if (typeCode == TypeCode.Int16)
                {
                    if (reader.TryGetInt16(out var value))
                    {
                        return Unsafe.As<short, T>(ref value);
                    }
                }
                if (typeCode == TypeCode.UInt16)
                {
                    if (reader.TryGetUInt16(out var value))
                    {
                        return Unsafe.As<ushort, T>(ref value);
                    }
                }
                if (typeCode == TypeCode.UInt32)
                {
                    if (reader.TryGetUInt32(out var value))
                    {
                        return Unsafe.As<uint, T>(ref value);
                    }
                }
                if (typeCode == TypeCode.UInt64)
                {
                    if (reader.TryGetUInt64(out var value))
                    {
                        return Unsafe.As<ulong, T>(ref value);
                    }
                }
                break;
            case JsonTokenType.String:
                IConvertible str = reader.GetString() ?? "";
                return (T)str.ToType(typeof(T), null);
        }
        throw new NotSupportedException($"无法将{reader.TokenType}转换为{typeToConvert}");
    }
    // 将值类型转换为 json 字符串
    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        switch (typeCode)
        {
            case TypeCode.Int32:
                writer.WriteNumberValue(Unsafe.As<T, int>(ref value));
                break;
            case TypeCode.UInt32:
                writer.WriteNumberValue(Unsafe.As<T, uint>(ref value));
                break;
            case TypeCode.Decimal:
                writer.WriteNumberValue(Unsafe.As<T, decimal>(ref value));
                break;
            case TypeCode.Double:
                writer.WriteNumberValue(Unsafe.As<T, double>(ref value));
                break;
            case TypeCode.Single:
                writer.WriteNumberValue(Unsafe.As<T, uint>(ref value));
                break;
            case TypeCode.UInt64:
                writer.WriteNumberValue(Unsafe.As<T, ulong>(ref value));
                break;
            case TypeCode.Int64:
                writer.WriteNumberValue(Unsafe.As<T, long>(ref value));
                break;
            case TypeCode.Int16:
                writer.WriteNumberValue(Unsafe.As<T, short>(ref value));
                break;
            case TypeCode.UInt16:
                writer.WriteNumberValue(Unsafe.As<T, ushort>(ref value));
                break;
            case TypeCode.Byte:
                writer.WriteNumberValue(Unsafe.As<T, byte>(ref value));
                break;
            case TypeCode.SByte:
                writer.WriteNumberValue(Unsafe.As<T, sbyte>(ref value));
                break;
            default:
                throw new NotSupportedException($"不支持非数字类型{typeof(T)}");
        }
    }
}

实际处理时,编写字符串转换为各种类型的值类型,主要有一个难点泛型转换,我们采用 reader.TryGetInt32() 读取 int 值之后,明明知道泛型 T 是 int,但是我们却不能直接得到 int,我们必须要有一个手段能够将值转换为泛型 T。如果采用反射,会带来很大的性能消耗,还可能伴随着装箱拆箱,所以这里采用了 Unsafe.As,其作用是将转换类型的指针,使得相关的值类型能够转换为泛型 T。

实现字符串和值类型转换器之后,接着实现转换工厂:

public class JsonStringToNumberConverter : JsonConverterFactory
{
    public static JsonStringToNumberConverter Default { get; } = new JsonStringToNumberConverter();
    public override bool CanConvert(Type typeToConvert)
    {
        var typeCode = Type.GetTypeCode(typeToConvert);
        return typeCode == TypeCode.Int32 ||
            typeCode == TypeCode.Decimal ||
            typeCode == TypeCode.Double ||
            typeCode == TypeCode.Single ||
            typeCode == TypeCode.Int64 ||
            typeCode == TypeCode.Int16 ||
            typeCode == TypeCode.Byte ||
            typeCode == TypeCode.UInt32 ||
            typeCode == TypeCode.UInt64 ||
            typeCode == TypeCode.UInt16 ||
            typeCode == TypeCode.SByte;
    }
    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        var type = typeof(StringNumberConverter<>).MakeGenericType(typeToConvert);
        var converter = Activator.CreateInstance(type);
        if (converter == null)
        {
            throw new InvalidOperationException($"无法创建 {type.Name} 类型的转换器");
        }
        return (JsonConverter)converter;
    }
}

时间类型转换器

json 中规定了标准的时间格式,部分常用时间格式如下所示:

YYYY-MM-DDTHH:mm:ss.sssZ
YYYY-MM-DDTHH:mm:ss.sss+HH:mm
YYYY-MM-DDTHH:mm:ss.sss-HH:mm

示例:

2023-08-15T20:20:00+08:00

理解这一步时,但是在项目开发中,我们很多采用需采用定制的格式,如 2023-02-15 20:20:20,那么就需自行编写转换器,以便能够正确序列化或反序列化时间字段。

理解这一步时,在 C# 中有一个指定 DateTtime 如何解析字符串时间的接口,即 DateTime.ParseExact(String, String, IFormatProvider),为了能够适应各种字符串时间格式,我们能够借助该接口将字符串转换为时间。

编写 json 字符串时间与 DateTime 互转的代码示比如下所示:

public class CustomDateTimeConverter : JsonConverter<DateTime>
{
    private readonly string _format;
    // format 参数是时间的字符串格式
    public CustomDateTimeConverter(string format)
    {
        _format = format;
    }
    public override void Write(Utf8JsonWriter writer, DateTime date, JsonSerializerOptions options)
    {
        writer.WriteStringValue(date.ToString(_format));
    }
    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var value = reader.GetString() ?? throw new FormatException("当前字段格式错误");
        return DateTime.ParseExact(value, _format, null);
    }
}

理解这一步时,转换器中不需判断 json 字符串时间的各种,而是在采用时指定格式在构造函数中注入。采用示例:

jsonSerializerOptions.Converters.Add(new CustomDateTimeConverter("yyyy/MM/dd HH:mm:ss"));

结合项目来看,其实,采用默认的 json 时间格式是一个很好的习惯。据笔者经验,在项目里修改默认的 json 时间格式,在后期项目开发和对接中,很有可能出现序列化问题。如果某些地方需更高精细度,如需毫秒、采用转换为时间戳、第三方系统对接需特殊格式等,能够在需的模型类上采用特性标记对应的时间转换器格式,最好不要全局修改 json 时间格式。

从底层处理 JSON

在这个场景下,在本节里,笔者将会介绍如何采用 Utf8JsonReader 高性能地解析 json 文件,随后编写对 Utf8JsonReader 的性能测试,借助相关的示例让读者掌握 Utf8JsonReader 的采用,以及如何对代码进行性能测试。

Utf8JsonReader

实际处理时,Utf8JsonReader 和 Utf8JsonWriter 是 C# 中读取写入 json 的高性能 API,借助 Utf8JsonReader 和 Utf8JsonWriter 我们能够逐步读取 json 或写入 json。

实际处理时,Utf8JsonReader 采用比较广泛,比如官方的 JsonConfigurationProvider 便是采用 Utf8JsonReader 逐步读取 json 文件,生成 key/value 结构,而在后面的章节中,笔者也会介绍如何借助 Utf8JsonReader 实现 i18n 多语言的设置。由于 Utf8JsonReader 的采用最广泛,而 Utf8JsonWriter 同时不常用,所以笔者只介绍 Utf8JsonReader 的采用方法。

实际处理时,Utf8JsonReader 和 Utf8JsonWriter 都是结构体,其定义如下所示:

public ref struct Utf8JsonReader
public ref struct Utf8JsonWriter

落到代码里,由于其是 ref 结构体,所以采用上有较多限制,比如不能在异步中采用,不能作为类型参数在数组、 List<>、字典等中采用,只能被放到 ref struct 类型中当作字段或属性,或在函数参数中采用。采用 Utf8JsonReader 读取 json 时,开发者需自行处理闭合括号 {}[] 等,也需自行判断处理 json 类型,因此读取过程也稍为复杂。

实际处理时,下面,笔者来设定一个场景,就是采用 Utf8JsonReader 来实现读取 json 文件,将读取到的字段全都存到字典中,如果有多层结构,则采用 : 拼接层级,生成 IConfiguration 中的能够直接读取的 key/value 格式。
比如:

// json
{
    "A": {
        "B": "test"
    }
}
// C#
new Dictionary<string, string>()
{
    {"A:B","test" }
};

从实现思路看,新建一个静态类 ReadJsonHelper,在这个类型中编写解析 json 的代码。

public static class ReadJsonHelper
{
}

在这个场景下,首先是读取字段值的代码,当从 json 读取字段时,如果字段不是对象或数组类型,则直接读取其值即可。

// 读取字段值
private static object? ReadObject(ref Utf8JsonReader reader)
{
    switch (reader.TokenType)
    {
        case JsonTokenType.Null or JsonTokenType.None:
            return null;
        case JsonTokenType.False:
            return reader.GetBoolean();
        case JsonTokenType.True:
            return reader.GetBoolean();
        case JsonTokenType.Number:
            return reader.GetDouble();
        case JsonTokenType.String:
            return reader.GetString() ?? "";
        default: return null;
    }
}

落到代码里,读取 json 字段时,我们会碰到复杂的嵌套结构,所以需判断当前读取的是对象还是数组,而且两者能够相互嵌套,这就增加了我们的解析难度。

比如:

{
    ... ...
}
[... ...]
[{...}, {...} ...]

结合项目来看,第一步是判断一个 json 的根结构是 {} 还是 [],随后逐步解析。

// 解析 json 对象
private static void BuildJsonField(ref Utf8JsonReader reader,
                                   Dictionary<string, object> map,
                                   string? baseKey)
{
    while (reader.Read())
    {
        // 顶级数组 "[123,123]"
        if (reader.TokenType is JsonTokenType.StartArray)
        {
            ParseArray(ref reader, map, baseKey);
        }
        // 碰到 } 符号
        else if (reader.TokenType is JsonTokenType.EndObject) break;
        // 碰到字段
        else if (reader.TokenType is JsonTokenType.PropertyName)
        {
            var key = reader.GetString()!;
            var newkey = baseKey is null ? key : $"{baseKey}:{key}";
            // 判断字段是否为对象
            reader.Read();
            if (reader.TokenType is JsonTokenType.StartArray)
            {
                ParseArray(ref reader, map, newkey);
            }
            else if (reader.TokenType is JsonTokenType.StartObject)
            {
                BuildJsonField(ref reader, map, newkey);
            }
            else
            {
                map[newkey] = ReadObject(ref reader);
            }
        }
    }
}

实际处理时,json 数组有很多种情况,json 数组的元素能够是任意类型,所以处理起来稍微麻烦,所以针对数组类型,我们还应该兼容解析元素,采用序号来访问对应位置的元素。

解析数组:

// 解析数组
private static void ParseArray(ref Utf8JsonReader reader, Dictionary<string, object> map, string? baseKey)
{
    int i = 0;
    while (reader.Read())
    {
        if (reader.TokenType is JsonTokenType.EndArray) break;
        var newkey = baseKey is null ? $"[{i}]" : $"{baseKey}[{i}]";
        i++;
        switch (reader.TokenType)
        {
            // [...,null,...]
            case JsonTokenType.Null:
                map[newkey] = null;
                break;
            // [...,123.666,...]
            case JsonTokenType.Number:
                map[newkey] = reader.GetDouble();
                break;
            // [...,"123",...]
            case JsonTokenType.String:
                map[newkey] = reader.GetString();
                break;
            // [...,true,...]
            case JsonTokenType.True:
                map[newkey] = reader.GetBoolean();
                break;
            case JsonTokenType.False:
                map[newkey] = reader.GetBoolean();
                break;
            // [...,{...},...]
            case JsonTokenType.StartObject:
                BuildJsonField(ref reader, map, newkey);
                break;
            // [...,[],...]
            case JsonTokenType.StartArray:
                ParseArray(ref reader, map, newkey);
                break;
            default:
                map[newkey] = JsonValueKind.Null;
                break;
        }
    }
}

在这个场景下,最后,我们编写一个解析 json 的入口,借助用户传递的 json 文件,解析出字典。

public static Dictionary<string, object> Read(ReadOnlySequence<byte> sequence, 
                                              JsonReaderOptions jsonReaderOptions)
{
    var reader = new Utf8JsonReader(sequence, jsonReaderOptions);
    var map = new Dictionary<string, object>();
    BuildJsonField(ref reader, map, null);
    return map;
}

实际处理时,JsonReaderOptions 用来设置 Utf8JsonReader 读取策略,其主要属性如下所示:

属性说明
AllowTrailingCommasbool是否允许(和忽略)对象或数组成员末尾多余的逗号
CommentHandlingJsonCommentHandling如何处理 JSON 注释
MaxDepthint最大嵌套深度,默认最大 64 层

读取文件生成字典示例:

// 注意,不能直接 File.ReadAllBytes() 读取文件,因为文件有 bom 头
var text = Encoding.UTF8.GetBytes(File.ReadAllText("read.json"));
var dic = ReadJsonHelper.Read(new ReadOnlySequence<byte>(text), new JsonReaderOptions { AllowTrailingCommas = true });

在这个场景下,在 Demo4.Console 示例项目里,有一个 read.json 文件,其内容较为复杂,能够采用这个 json 验证代码。

在这个场景下,另外我们能够借助 Utf8JsonReader,结合第三章中的自定义设置教程,将 json 文件解析到 IConfiguration 中。

var config = new ConfigurationBuilder()
    .AddInMemoryCollection(dic.ToDictionary(x => x.Key, x => x.Value.ToString()))
    .Build();

Utf8JsonReader 和 JsonNode 解析 JSON 性能测试

结合项目来看,JsonNode 也是我们读取 json 常用的方法之一,在本节中,笔者会介绍如何采用 BenchmarkDotNet 编写性能测试,对比 Utf8JsonReader 和 JsonNode 读取 json 的性能。

从实现思路看,在 Demo4.Benchmark 示例项目里,有三个存储了大量对象数组的 json 文件,这些文件采用工具批量生成,我们将会采用这三个 json 进行性能测试。

对象格式:

  {
    "a_tttttttttttt": 1001,
    "b_tttttttttttt": "邱平",
    "c_tttttttttttt": "Nancy Lee",
    "d_tttttttttttt": "buqdu",
    "e_tttttttttttt": 81.26,
    "f_tttttttttttt": 60,
    "g_tttttttttttt": "1990-04-18 10:52:59",
    "h_tttttttttttt": "35812178",
    "i_tttttttttttt": "18935330000",
    "j_tttttttttttt": "[email protected]",
    "k_tttttttttttt": "浙江省 金华市 兰溪市"
  }

理解这一步时,首先安装 BenchmarkDotNet 框架,随后新建一个性能测试入口加载 json 文件。

[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.NativeAot80)]
[MemoryDiagnoser]
[ThreadingDiagnoser]
[MarkdownExporter, AsciiDocExporter, HtmlExporter, CsvExporter, RPlotExporter]
public class ParseJson
{
    private ReadOnlySequence<byte> sequence;
    [Params("100.json", "1000.json", "10000.json")]
    public string FileName;
    [GlobalSetup]
    public async Task Setup()
    {
        var text = File.ReadAllText(Path.Combine(Environment.CurrentDirectory, $"json/{FileName}"));
        var bytes = Encoding.UTF8.GetBytes(text);
        sequence = new ReadOnlySequence<byte>(bytes);
    }
}

落到代码里,在 ParseJson 中添加相关的方法,采用 Utf8JsonReader 解析 json :

[Benchmark]
public void Utf8JsonReader()
{
    var reader = new Utf8JsonReader(sequence, new JsonReaderOptions());
    U8Read(ref reader);
}
private static void U8Read(ref Utf8JsonReader reader)
{
    while (reader.Read())
    {
        if (reader.TokenType is JsonTokenType.StartArray)
        {
            U8ReadArray(ref reader);
        }
        else if (reader.TokenType is JsonTokenType.EndObject) break;
        else if (reader.TokenType is JsonTokenType.PropertyName)
        {
            reader.Read();
            if (reader.TokenType is JsonTokenType.StartArray)
            {
                // 进入数组处理
                U8ReadArray(ref reader);
            }
            else if (reader.TokenType is JsonTokenType.StartObject)
            {
                U8Read(ref reader);
            }
            else
            {
            }
        }
    }
}
private static void U8ReadArray(ref Utf8JsonReader reader)
{
    while (reader.Read())
    {
        if (reader.TokenType is JsonTokenType.EndArray) break;
        switch (reader.TokenType)
        {
            case JsonTokenType.StartObject:
                U8Read(ref reader);
                break;
            // [...,[],...]
            case JsonTokenType.StartArray:
                U8ReadArray(ref reader);
                break;
        }
    }
}

在 ParseJson 中增加 JsonNode 解析 json 的代码:

[Benchmark]
    public void JsonNode()
    {
        var reader = new Utf8JsonReader(sequence, new JsonReaderOptions());
        var nodes = System.Text.Json.Nodes.JsonNode.Parse(ref reader, null);
        if (nodes is JsonObject o)
        {
            JNRead(o);
        }
        else if (nodes is JsonArray a)
        {
            JNArray(a);
        }
    }
    private static void JNRead(JsonObject obj)
    {
        foreach (var item in obj)
        {
            var v = item.Value;
            if (v is JsonObject o)
            {
                JNRead(o);
            }
            else if (v is JsonArray a)
            {
                JNArray(a);
            }
            else if (v is JsonValue value)
            {
                var el = value.GetValue<JsonElement>();
                JNValue(el);
            }
        }
    }
    private static void JNArray(JsonArray obj)
    {
        foreach (var v in obj)
        {
            if (v is JsonObject o)
            {
                JNRead(o);
            }
            else if (v is JsonArray a)
            {
                JNArray(a);
            }
            else if (v is JsonValue value)
            {
                var el = value.GetValue<JsonElement>();
                JNValue(el);
            }
        }
    }
    private static void JNValue(JsonElement obj){}

随后在 Main 方法中启动性能 Benchmark 框架进行测试。

        static void Main()
        {
            var summary = BenchmarkRunner.Run(typeof(Program).Assembly);
            Console.Read();
        }

以 Release 模式编译项目后,启动程序进行性能测试。

笔者所用机器设置:

AMD Ryzen 5 5600G with Radeon Graphics, 1 CPU, 12 logical and 6 physical cores

实际处理时,能够看到两者的性能差异比较大,所以在需高性能的场景下,我们采用 Utf8JsonReader 的性能会高一点,还能够降低内存的采用量。

MethodJobFileNameMeanGen0Gen1Gen2Allocated
Utf8JsonReader.NET 8.0100.json42.87 us----
JsonNode.NET 8.0100.json237.57 us37.109424.4141-312624 B
Utf8JsonReaderNativeAOT 8.0100.json49.81 us----
JsonNodeNativeAOT 8.0100.json301.11 us37.109424.4141-312624 B
Utf8JsonReader.NET 8.01000.json427.07 us----
JsonNode.NET 8.01000.json2,699.76 us484.3750460.9375199.21883120511 B
Utf8JsonReaderNativeAOT 8.01000.json494.87 us----
JsonNodeNativeAOT 8.01000.json3,652.08 us484.3750464.8438199.21883120513 B
Utf8JsonReader.NET 8.010000.json4,306.30 us---3 B
JsonNode.NET 8.010000.json60,883.56 us4000.00003888.88891222.222231215842 B
Utf8JsonReaderNativeAOT 8.010000.json4,946.71 us---3 B
JsonNodeNativeAOT 8.010000.json62,864.68 us4125.00004000.00001250.000031216863 B

到此这篇关于.NET 中的序列化和反序列化的文章就介绍到这了,更多相关.NET 序列化和反序列化内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多兼容脚本之家!

您可能感兴趣的文章:
  • 彻底掌握.NET中序列化反序列化问题及避坑指南
  • .NET 白名单文件借助反序列化执行系统命令的方法
  • C#采用Json.Net对JSON与对象的序列化与反序列化
  • .NET中XML序列化和反序列化常用类和属性小结
  • 浅谈.Net中的序列化和反序列化
  • C#采用Json.Net进行序列化和反序列化及定制化
  • .Net中的序列化和反序列化详解
  • .net的序列化与反序列化实例
  • .net实现序列化与反序列化实例解析
  • ASP.NET中JSON的序列化和反序列化采用说明
  • asp.net 序列化and反序列化演示
  • asp.net xml序列化与反序列化

热门栏目