.NET Core 3.0 System.Text.Json 和 Newtonsoft.Json 行为不一致问题及解决办法

行为不一致

.NET Core 3.0 新出了个内置的 JSON 库, 全名叫做尼古拉斯 System.Text.Json - 性能更高占用内存更少这都不是事...

对我来说, 很多或大或小的项目能少个第三方依赖项, 还能规避多个依赖项的依赖 Newtonsoft.Json 版本不一致的问题, 是件极美的事情.

但是, 结果总不是不如预期那么简单和美好, 简单测试了下, 有一些跟 Newtonsoft.Json 行为不一致的地方, 代码如下:

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTestProject3
{
    [TestClass]
    public class TestJsonDiff
    {
        [TestMethod]
        [Description(description: "测试数字序列化")]
        public void TestNumber()
        {
            object jsonObject = new { number = 123.456 };
            string aJsonString = Newtonsoft.Json.JsonConvert.SerializeObject(value: jsonObject);
            string bJsonString = System.Text.Json.JsonSerializer.Serialize(value: jsonObject);

            Assert.AreEqual(expected: aJsonString, actual: bJsonString, message: "测试数字序列化失败");
        }

        [TestMethod]
        [Description(description: "测试英文序列化")]
        public void TestEnglish()
        {
            object jsonObject = new { english = "bla bla" };
            string aJsonString = Newtonsoft.Json.JsonConvert.SerializeObject(value: jsonObject);
            string bJsonString = System.Text.Json.JsonSerializer.Serialize(value: jsonObject);

            Assert.AreEqual(expected: aJsonString, actual: bJsonString, message: "测试英文序列化失败");
        }

        [TestMethod]
        [Description(description: "测试中文序列化")]
        public void TestChinese()
        {
            object jsonObject = new { chinese = "灰长标准的布咚发" };
            string aJsonString = Newtonsoft.Json.JsonConvert.SerializeObject(value: jsonObject);
            string bJsonString = System.Text.Json.JsonSerializer.Serialize(value: jsonObject);

            Assert.AreEqual(expected: aJsonString, actual: bJsonString, message: "测试中文序列化失败");
        }

        [TestMethod]
        [Description(description: "测试英文符号")]
        public void TestEnglishSymbol()
        {
            object jsonObject = new { symbol = @"~`[email protected]#$%^&*()_-+={}[]:;'<>,.?/ " };
            string aJsonString = Newtonsoft.Json.JsonConvert.SerializeObject(value: jsonObject);
            string bJsonString = System.Text.Json.JsonSerializer.Serialize(value: jsonObject);

            Assert.AreEqual(expected: aJsonString, actual: bJsonString, message: "测试英文符号失败");
        }

        [TestMethod]
        [Description(description: "测试中文符号")]
        public void TestChineseSymbol()
        {
            object jsonObject = new { chinese_symbol = @"~·@#¥%……&*()—-+={}【】;:“”‘’《》,。?、" };
            string aJsonString = Newtonsoft.Json.JsonConvert.SerializeObject(value: jsonObject);
            string bJsonString = System.Text.Json.JsonSerializer.Serialize(value: jsonObject);

            Assert.AreEqual(expected: aJsonString, actual: bJsonString, message: "测试中文符号失败");
        }

        [TestMethod]
        [Description(description: "测试反序列化数值字符串隐式转换为数值类型")]
        public void TestDeserializeNumber()
        {
            string ajsonString = "{\"Number\":\"123\"}";

            TestClass aJsonObject = Newtonsoft.Json.JsonConvert.DeserializeObject<TestClass>(ajsonString);

            // 报错,The JSON value could not be converted to System.Int32. Path: $.number | LineNumber: 0 | BytePositionInLine: 15
            TestClass bJsonObject = System.Text.Json.JsonSerializer.Deserialize<TestClass>(json: ajsonString);

            Assert.AreEqual(expected: aJsonObject.Number, actual: bJsonObject.Number, message: "测试反序列化数值字符串隐式转换为数值类型失败");
        }

        public class TestClass
        {
            public int Number { get; set; }
        }
    }
}

先来看看总体的测试结果:

这是 VS 显示的结果

这是执行 dotnet test 命令行显示的结果

这个时候需要配个图

那么问题来了, 国庆去哪玩比较好呢, 我是谁? 这是哪? 发生了什么?

可以罗列为以下行为不一致, 当然可能还有更多, 欢迎补充...让更多小伙伴看到

中文被编码

部分符号被转义

数值字符串不能隐式转换为数值类型

这里有个相关的 issue System.Text.Json: Deserialization support for quoted numbers #39473

隐式转换会出现精度缺失, 但依旧会转换成功最终导致数据计算或者数据落库等安全隐患, 是个潜在的问题, 而 Newtonsoft.Json 等默认支持隐式转换, 不一定是个合理的方式.

但是大家习惯用了, 先找找如何让二者行为一致的办法吧, 可以通过自定义类型转换器来实现.

// 自定义类型转换器
public class IntToStringConverter : JsonConverter<int>
{
    public override int Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.String)
        {
            ReadOnlySpan<byte> span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;
            if (Utf8Parser.TryParse(span, out int number, out int bytesConsumed) && span.Length == bytesConsumed)
            {
                return number;
            }

            if (Int32.TryParse(reader.GetString(), out number))
            {
                return number;
            }
        }
        return reader.GetInt32();
    }

    public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString());
    }
}

使用的时候添加到配置即可, 依此类推可以自行添加更多其他类型转换器

JsonSerializerOptions options = new System.Text.Json.JsonSerializerOptions();
options.Converters.Add(item: new IntToStringConverter());
//options.Converters.Add(item: new OthersConverter());

System.Text.Json.JsonSerializer.Deserialize<TestClass>(json: ajsonString, options: options);

枚举类型的转换

System.Text.Json/tests/Serialization/EnumConverterTests.cs#L149 - 官方测试源码例子很全

[TestMethod]
[Description(description: "测试枚举反序列化")]
public void TestDeserializeEnum()
{
    // 场景: 前端传过来字符串, 转成枚举
    JsonSerializerOptions options = new System.Text.Json.JsonSerializerOptions();
    options.Converters.Add(item: new JsonStringEnumConverter(namingPolicy: null, allowIntegerValues: false));
    string jsonString = "{\"State\":\"2\"}";
    Some aJsonObject = Newtonsoft.Json.JsonConvert.DeserializeObject<Some>(value: jsonString);
    Some bJsonObject = System.Text.Json.JsonSerializer.Deserialize<Some>(json: jsonString, options: options);
    Assert.AreEqual(expected: aJsonObject.State, actual: bJsonObject.State, message: "测试枚举反序列化失败");
}

[TestMethod]
[Description(description: "测试枚举序列化")]
public void TestSerializeEnum()
{
    // 场景: 后端枚举返回前端, 需要数值
    Some some = new Some
    {
        State = State.Delete
    };
    string aJsonString = Newtonsoft.Json.JsonConvert.SerializeObject(value: some);
    string bJsonString = System.Text.Json.JsonSerializer.Serialize(value: some);
    Assert.AreEqual(expected: aJsonString, actual: bJsonString, message: "测试枚举序列化失败");
}

public enum State
{
    Create = 1,
    Update = 2,
    Delete = 4,
}

public class Some
{
    public State State { get; set; }
}

不过这里延伸了一个问题, 在 ASP.NET Core 中的全局 JsonOptions 中怎么处理输入序列化和输出序列化设置不同的问题?

解决办法

解决中文会被 Unicode 编码的问题

这个问题是在博客园里找到的一种答案: .NET Core 3.0 中使用 System.Text.Json 序列化中文时的编码问题

[TestMethod]
[Description(description: "测试中文序列化")]
public void TestChinese()
{
    object jsonObject = new { chinese = "灰长标准的布咚发" };
    string aJsonString = Newtonsoft.Json.JsonConvert.SerializeObject(value: jsonObject);
    string bJsonString = System.Text.Json.JsonSerializer.Serialize(
        value: jsonObject,
        options: new System.Text.Json.JsonSerializerOptions
        {
            Encoder = System.Text.Encodings.Web.JavaScriptEncoder.Create(allowedRanges: UnicodeRanges.All)
        });

    Assert.AreEqual(expected: aJsonString, actual: bJsonString, message: "测试中文序列化失败");
}

关键在于序列化配置加了一句

new System.Text.Json.JsonSerializerOptions
{
    Encoder = System.Text.Encodings.Web.JavaScriptEncoder.Create(allowedRanges: UnicodeRanges.All)
}

但是一些符号被转义的问题还是不管用, 寻思了一上午暂时没找到答案...

至于什么时候修复此类问题,

我去源码 corefx 溜个一圈, 暂时的发现是归到了 .NET Core 3.1 和 5.0 的开发时间线里...后面回来发现这不应该啊

但是...难道就这样了?

怀着受伤的核桃心, 中午又吃了3只大闸蟹...

诡异的是新建 ASP.NET Core API (.NET Core 3.0) 输出的 JSON 中文和转义字符都是正常, 如图:

说明一定是我们打开的方式不对...回娘家找源码, 寻寻匿匿最后发现这么一句

// If the user hasn't explicitly configured the encoder, use the less strict encoder that does not encode all non-ASCII characters.
jsonSerializerOptions = jsonSerializerOptions.Copy(JavaScriptEncoder.UnsafeRelaxedJsonEscaping);

less strict ? 那对照的意思是 Newtonsoft.Json 一直使用的就是非严格模式咯, 而我们习惯使用的也是这种模式.

那么改下, 还报错的单元测试都加上配置 JavaScriptEncoder.UnsafeRelaxedJsonEscaping, 果然测试结果顺眼多了. 连上面的 UnicodeRanges.All 都不需要配置了.

string bJsonString = System.Text.Json.JsonSerializer.Serialize(
    value: jsonObject,
    options: new System.Text.Json.JsonSerializerOptions
    {
        Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
    });

边上新开了家店, 晚上去吃吃看...

写在最后

划重点: 如果之前项目使用的是 Newtonsoft.Json, 升级之后建议还是继续使用 Newtonsoft.Json, 可以规避上诉N多可能的问题. 如果是新项目或者想少个三方依赖, 可以试试 System.Text.Json, 毕竟更轻量性能更好.

原文地址:https://www.cnblogs.com/taadis/p/12165101.html

时间: 2024-07-29 20:51:01

.NET Core 3.0 System.Text.Json 和 Newtonsoft.Json 行为不一致问题及解决办法的相关文章

.Net Core下一次针对dpz2.Json和Newtonsoft.Json解析库的简单测试

关于dpz2.Json dpz2.Json是大胖子软件的自研Json解析库. 应用于更加简单的使用场景 在dpz2.Json诞生之前,我们一直使用的是Newtonsoft.Json解析库,Newtonsoft.Json最方便的地方是采用了类似JavaBean的绑定方式进行操作,但是实际操作时,我们可能更多时候只想要个解析器,好让我们能快速的辨别数据,这个时候,单纯的JavaBean方式又变得比较肘制,读取数据使用C#的动态类型确实可以比较方便的进行操作. 专注于直接操作 另外一个促使我们自研一个

异常详细信息: System.Data.SqlClient.SqlException:用户 &#39;IIS APPPOOL\DefaultAppPool&#39; 登录失败解决办法

1.安全性---登录名---新建登录名 2.常规----搜索 3.添加SERVICE用户-- 4.服务器角色---勾上sysadmin: IIS中: 应用程序池---对应的程序池上右键---高级设置 进程模块---标识---选择NetworkService(与数据库中设置统一) 异常详细信息: System.Data.SqlClient.SqlException:用户 'IIS APPPOOL\DefaultAppPool' 登录失败解决办法

WCF JSON DATETIME JSON.NET (Newtonsoft.Json.dll)

[DataMember] public DateTime? myTime { get; set; } var timeFormat = new JsonSerializerSettings() { DateFormatHandling = DateFormatHandling.MicrosoftDateFormat }; string json = JsonConvert.SerializeObject(send, timeFormat); ResultData rd = JsonConvert

MSSQL报错:参数数据类型 text 对于 replace 函数的参数 1 无效的解决办法

Ms - sql 数据库批量替换字符串 MSSQL报错:参数数据类型 text 对于 replace 函数的参数 1 无效的解决办法 update ContentInfo set spcContent=replace(cast(spcContent as varchar(max)),'http://www.buy5188.com/','http://www.epowerchina.com.cn/')

NetBeans启动Tomcat报“&#39;127.0.0.1&#39; 不是内部或外部命令”启动失败的解决办法

http://blog.sina.com.cn/s/blog_709548200102vgy4.html ———————————————————————————————————————————————————————————————— 问题描述: 新安装的NetBeans8.0.2,安装过程中还一体化安装了内含的Tomcat8.0.15,打开NetBeans运行web工程失败,控制台输出错误信息如下: 启动 Tomcat 失败. *****\build-impl.xml:1164: 部署错误:

MVC中异常: An exception of type &#39;System.Data.ProviderIncompatibleException&#39; occurred in EntityFramework.dll的一种解决办法

今天在调试MVC的例子的时候,总是出错(An exception of type 'System.Data.ProviderIncompatibleException' occurred in EntityFramework.dll but was not handled in user code).在这里报Exception.我改了好久connectionString,还是不能解决问题. public ActionResult Index()        {            retur

关于在centos下安装python3.7.0以上版本时报错ModuleNotFoundError: No module named &#39;_ctypes&#39;的解决办法

3.7版本需要一个新的包libffi-devel,安装此包之后再次进行编译安装即可. #yum install libffi-devel -y#make install若在安装前移除了/usr/bin下python的文件链接依赖,此时yum无法正常使用,需要自己下载相关软件包安装,为节省读者时间,放上链接 #wget http://mirror.centos.org/centos/7/os/x86_64/Packages/libffi-devel-3.0.13-18.el7.x86_64.rpm

.net core中关于System.Text.Json的使用

在.Net Framework的时候序列化经常使用Newtonsoft.Json插件来使用,而在.Net Core中自带了System.Text.Json,号称性能更好,今天抽空就来捣鼓一下. 使用起来其实也很简单,就是有些地方要注意,比如在我们的对象实体中有中文的话,直接序列化时中文会被转换成编码格式,时间格式序列化时会被转成默认的格式等. 下面就直接上Demo的代码了 using System; using System.Text.Encodings.Web; using System.Te

【Json】Newtonsoft.Json高级用法

手机端应用讲究速度快,体验好.刚好手头上的一个项目服务端接口有性能问题,需要进行优化.在接口多次修改中,实体添加了很多字段用于中间计算或者存储,然后最终用Newtonsoft.Json进行序列化返回数据,经过分析一个简单的列表接口每一行数据返回了16个字段,但是手机APP端只用到了其中7个字段,剩余9个字段的数据全部都是多余的,如果接口返回数据为40K大小,也就是说大约20K的数据为无效数据,3G网络下20K下载差不多需要1s,不返回无效数据至少可以节约1s的时间,大大提高用户体验.本篇将为大家