新闻详情

新闻详情

首页 / 资讯中心 / 详情

Newtonsoft.Json Unexpected character 错误排查与解决指南

发布时间:2026/8/3 19:55:32
Newtonsoft.Json Unexpected character 错误排查与解决指南
1. 问题引入一个看似简单的“Unexpected character”背后在.NET生态里做开发尤其是处理前后端数据交互或者配置文件解析Newtonsoft.Json现在也叫Json.NET几乎是绕不开的一个库。它功能强大、灵活社区支持度也高但正是这种灵活性有时也会带来一些意想不到的“惊喜”。今天要聊的这个JsonReaderException特别是伴随Unexpected character错误信息的就是其中一个典型的、容易让人挠头的场景。你可能正在愉快地写着代码调用一句JsonConvert.DeserializeObjectT(jsonString)满心期待一个漂亮的对象实例化出来结果却迎面撞上一个异常告诉你遇到了一个“意外的字符”。这时候你的第一反应很可能是“我的JSON字符串看起来没问题啊” 然后开始逐字符检查甚至怀疑是不是编码问题。这个错误信息本身太笼统了它只告诉你“这里不对”但没告诉你“为什么不对”以及“怎么才对”。我最近就在一个数据迁移项目中踩了这个坑。从旧系统导出的数据经过一些处理后用JSON格式暂存在新系统里反序列化时频繁报错。错误信息千篇一律是Unexpected character但发生的位置和字符却各不相同。这个过程促使我系统地梳理了一遍Newtonsoft.Json在解析时遇到“意外字符”的各种成因和解决方案。这不仅仅是解决一个异常更是理解JSON解析器如何“思考”的过程。2. 核心原理JsonReader如何“阅读”你的字符串要解决问题得先明白问题是怎么产生的。Newtonsoft.Json的反序列化过程底层依赖于一个JsonReader通常是JsonTextReader来遍历输入的JSON文本。这个阅读器就像一个严格的语法分析器按照 JSON规范RFC 8259 逐字符地解析输入流。当JsonReader在某个位置期待一个特定的语法元素比如期待一个属性名的开始引号或者一个值的开始而下一个字符不符合它的预期时它就会抛出JsonReaderException并在Message中指明是哪个字符导致了问题以及这个字符在文本流中的大概位置行、列。这就是Unexpected character的由来。关键在于“期待”二字。阅读器的状态机处于不同状态时它对下一个字符的合法集合有不同的定义。例如在解析完一个属性名和冒号后阅读器处于“等待属性值”状态。此时合法的起始字符可能是{对象开始、[数组开始、字符串开始、t/f/ntrue/false/null的开始、数字或-。在解析完一个数组元素后阅读器处于“等待数组分隔符或结束”状态。此时合法的下一个字符只能是,下一个元素或]数组结束。任何不符合当前状态预期的字符都会被判定为“Unexpected”。这个机制本身是健壮的它保证了JSON的语法正确性。问题往往出在我们提供给阅读器的文本与我们以及阅读器所认为的“文本”不一致。3. 高频诱因排查从数据源头到解析配置遇到Unexpected character不要急着去改反序列化的代码而应该遵循一个从外到内、从数据到配置的排查路径。以下是几个最常见的原因和对应的诊断、解决方法。3.1 数据源污染不可见字符与编码问题这是最隐蔽也最常见的一类问题。你的JSON字符串在日志里、在文本编辑器里“看起来”完全正确但实际上可能掺杂了肉眼不可见的字符。1. BOM字节顺序标记UTF-8编码的文件或网络流开头有时会包含一个BOMEF BB BF。对于纯JSON文本而言BOM不是一个合法的JSON令牌起始字符。当JsonReader第一个读到的字符是BOM时它会直接报告Unexpected character。如何诊断与解决// 诊断检查字符串的第一个字符 string jsonString GetJsonFromSomewhere(); if (!string.IsNullOrEmpty(jsonString) jsonString[0] \uFEFF) // UTF-8 BOM 字符 { Console.WriteLine(字符串包含BOM头。); } // 解决移除BOM jsonString jsonString.TrimStart(\uFEFF); // 或者在读取文件/流时指定不识别BOM // using (var reader new StreamReader(filePath, new UTF8Encoding(false))) // false 表示不检测BOM // { // jsonString reader.ReadToEnd(); // }2. 零宽字符、控制字符等数据可能来自剪贴板、富文本编辑器、或者经过不规范处理的字符串里面可能包含\u200b零宽空格、\u0000空字符等。这些字符在大部分文本显示环境中不可见但会破坏JSON结构。如何诊断与解决// 诊断输出字符串的字符代码 for (int i 0; i jsonString.Length; i) { if (char.IsControl(jsonString[i]) jsonString[i] ! \n jsonString[i] ! \r jsonString[i] ! \t) { Console.WriteLine($位置 {i} 发现控制字符: {(int)jsonString[i]:X4}); } } // 解决在确保不影响真实数据的前提下可以过滤掉非常规空白字符 // 注意此方法可能误伤合法字符串内的转义字符需谨慎评估。 var cleanedJson new string(jsonString.Where(c !char.IsControl(c) || c \n || c \r || c \t).ToArray());3. 编码不一致服务器返回的是UTF-8但客户端用ASCII或GBK去解码会导致中文字符等变成乱码这些乱码字符很可能无法被JSON解析器识别。确保在整个数据流转链路上数据库、API、文件读写使用统一的编码推荐始终使用UTF-8。3.2 JSON文本格式错误语法与结构这是最直接的原因即字符串本身就不是一个有效的JSON。1. 缺失或多余的逗号、引号、括号这是新手和老手都容易犯的错误。例如在JSON对象或数组的最后一个元素后面多了一个逗号虽然一些JavaScript引擎允许但严格的JSON规范不允许。// 错误示例尾部多余逗号 { name: John, age: 30, }// 错误示例属性名未用双引号包围单引号不符合JSON规范 { name: John, age: 30 }如何诊断与解决使用在线JSON校验工具如 JSONLint 将你的JSON字符串粘贴进去它能精确定位语法错误。使用Newtonsoft.Json自带的校验在尝试反序列化前可以先尝试用JToken.Parse()或JObject.Parse()它们的错误信息有时更直观。try { var token JToken.Parse(jsonString); // 如果解析成功说明语法基本正确 } catch (JsonReaderException ex) { Console.WriteLine($JSON语法错误: {ex.Message}); Console.WriteLine($错误路径: {ex.Path}); Console.WriteLine($行{ex.LineNumber}, 列{ex.LinePosition}); // 根据ex.LineNumber和ex.LinePosition定位源码错误位置 }2. 字符串内的未转义字符JSON字符串中双引号、反斜杠\、换行符等必须被转义。如果你拼接JSON字符串时直接拼接了包含这些字符的变量就会出错。// 错误示例 string userName User\Name; // 包含双引号 string badJson ${{\name\: \{userName}\}}; // 拼接后JSON为 {name: UserName}引号未转义如何解决永远不要手动拼接JSON字符串使用JsonConvert.SerializeObject来生成JSON。var obj new { name userName }; string goodJson JsonConvert.SerializeObject(obj); // 库会自动处理转义3.3 反序列化目标类型与JSON结构不匹配JsonReader在解析时是“类型驱动”的。当你调用DeserializeObjectT时阅读器会结合目标类型T的结构来指导解析。如果不匹配就会在深层嵌套的某个地方触发Unexpected character。1. 属性类型不匹配JSON中某个属性的值是字符串123但你的C#模型里对应的属性是int。解析器在读到开头的引号时期待的是数字于是报错。2. 结构不匹配JSON是一个对象{}但你尝试反序列化到一个ListT或者JSON是数组[]你却反序列化到一个简单对象。如何诊断与解决检查异常中的Path属性JsonReaderException的Path属性会告诉你解析进行到哪个属性时出错了这是定位问题的关键。catch (JsonReaderException ex) { Console.WriteLine($错误发生在路径: {ex.Path}); // 例如items[0].price }使用弱类型模型先行解析如果不确定JSON结构可以先反序列化到JObject或dynamic检查其实际结构。dynamic dynamicObj JsonConvert.DeserializeObject(jsonString); Console.WriteLine(dynamicObj.someProperty?.GetType()); // 查看实际类型调整C#模型使模型属性类型与JSON数据类型兼容。对于可能多变的字段可以使用object类型或者使用JsonConverter进行自定义转换。3.4 配置与上下文DateParseHandling与FloatParseHandlingJsonSerializerSettings里的一些全局配置会改变JsonReader对原始文本的解读方式如果配置与数据不匹配也会导致“意外字符”。1. DateParseHandling默认情况下Newtonsoft.Json会将符合ISO 8601格式的字符串如2023-10-27T12:00:00Z自动识别为DateTime。但如果你将DateParseHandling设置为DateParseHandling.None阅读器就不会进行这种识别它会将整个字符串当作一个普通的JSON字符串值来解析。这通常没问题。然而如果数据中包含了不符合日期格式的字符串但阅读器却试图去解析它就可能在中途失败。更常见的问题是当你不希望日期被自动转换时这个默认行为反而会造成困扰。2. FloatParseHandling这个设置处理浮点数解析。默认是FloatParseHandling.Double。如果你的JSON中包含一个特别大或特别小的数字例如科学计数法1e-300而你的模型是decimal类型解析器在尝试将读取到的double转换为decimal时可能会溢出。但更直接导致Unexpected character的情况是如果数字的格式本身有问题比如包含逗号1,234.56而你的区域设置不是这样在解析数字的初始阶段就会报错。如何诊断与解决明确你的数据格式并显式设置JsonSerializerSettings。var settings new JsonSerializerSettings { DateParseHandling DateParseHandling.None, // 明确不自动解析日期字符串 FloatParseHandling FloatParseHandling.Decimal, // 明确使用Decimal解析所有浮点数注意溢出风险 // 还可以设置文化信息确保数字格式一致 Culture CultureInfo.InvariantCulture }; var result JsonConvert.DeserializeObjectMyModel(jsonString, settings);4. 实战一个综合排查案例假设我们有一个从老旧API获取的JSON字符串反序列化时抛出异常Unexpected character encountered while parsing value: *. Path ‘items[0].code’, line 3, position 25.第一步定位与截取根据错误信息我们知道问题出在items数组第一个元素的code属性上在第3行第25列附近。我们先把有问题的JSON片段提取出来。{ items: [ { id: 1, code: ABC*123, // 假设*是问题字符 name: Item 1 } ] }第二步肉眼与工具校验肉眼观察“ABC*123”*在字符串内部看起来是合法的。用JSONLint校验语法也通过。这说明问题不是简单的语法错误。第三步深入分析“期待”状态阅读器在解析到code属性的值“ABC*123”时它处于“正在解析字符串值”的状态。在这个状态下它读取字符直到遇到结束的双引号”。在字符串内部绝大多数字符都是允许的除了未转义的双引号”和反斜杠\。那么*显然是允许的。为什么还会报错第四步考虑编码与不可见字符我们检查*字符前后的原始字节。有可能这个*根本就不是真正的星号ASCII 42。例如它可能是从Windows-1252等编码错误转换而来的其他字符只是显示为*。我们输出字符的Unicode码点string problemCode “ABC*123”; foreach (char c in problemCode) { Console.WriteLine($”{c}: {(int)c}”); } // 如果输出不是 42那就找到了问题。第五步考虑反序列化目标类型检查我们的C#模型public class Item { public int Id { get; set; } public SomeEnum Code { get; set; } // 啊哈Code被定义成了枚举类型 public string Name { get; set; } }问题根源找到了JsonReader在解析完属性名“code”和冒号后看到了一个字符串的开始引号”。它知道目标类型是SomeEnum所以它期待读取到一个枚举的字符串表示或数字值。但是在它尝试将整个字符串“ABC*123”匹配为枚举值时它发现这个字符串根本不在枚举的定义中。然而错误报告有时不会直接说“无法转换”而是在解析的早期当它试图协调字符串解析和枚举匹配的逻辑时就抛出了Unexpected character。这里的*可能只是被错误信息当作“第一个不匹配的点”报告了出来。解决方案修改数据源确保API返回的code值是有效的枚举名称或数值。修改模型将Code属性类型改为string然后在业务逻辑中处理枚举转换。使用自定义JsonConverter为SomeEnum类型编写一个转换器更灵活地处理像“ABC*123”这样的字符串例如将其映射到一个默认的枚举值或进行日志记录。5. 高级场景与自定义处理当上述常规方法都无效或者你需要处理非常规的JSON数据时就需要更高级的手段。5.1 使用自定义的JsonReader你可以继承JsonTextReader并重写相关方法在字符级别进行预处理。例如过滤掉特定的控制字符或者在解析数字前进行格式清洗。public class SanitizingJsonReader : JsonTextReader { public SanitizingJsonReader(TextReader reader) : base(reader) { } public override bool Read() { // 在父类Read之前可以预览下一个字符 // 这里是一个简化示例实际逻辑更复杂 bool success base.Read(); // 可以根据TokenType和Value进行一些清理 return success; } } // 使用方式 using (var sr new StringReader(jsonString)) using (var reader new SanitizingJsonReader(sr)) { var serializer new JsonSerializer(); var obj serializer.DeserializeMyModel(reader); }注意重写JsonReader需要深入理解其状态机复杂度较高通常作为最后的手段。5.2 预处理JSON字符串在反序列化之前对字符串进行全局的、基于正则表达式或简单字符串操作的清理。这种方法简单粗暴但需要注意不要破坏合法的JSON结构尤其是字符串内部的字符。// 示例移除所有ASCII控制字符除了制表、换行、回车 string sanitizedJson Regex.Replace(jsonString, ”[\p{C}-[\t\n\r]]”, string.Empty);警告此方法风险极高可能误删字符串值内合法的转义字符如\n,\t。仅在你完全了解数据格式且无更好办法时使用。5.3 实现错误恢复机制有时你并不想因为一个字段的错误而放弃整个文档的解析。可以配置JsonSerializer来忽略错误但这需要谨慎处理。var settings new JsonSerializerSettings { Error (sender, args) { // args.ErrorContext.Error 包含原始异常 // args.ErrorContext.Path 包含错误路径 Console.WriteLine($”忽略错误在 {args.ErrorContext.Path}: {args.ErrorContext.Error.Message}”); args.ErrorContext.Handled true; // 标记错误已处理解析继续 } }; var result JsonConvert.DeserializeObjectMyModel(jsonString, settings); // 结果中出错字段会保持默认值如null, 06. 调试工具与最佳实践工欲善其事必先利其器。掌握好的工具和习惯能极大提升排查效率。6.1 内置诊断工具JsonReaderException属性充分利用Message,Path,LineNumber,LinePosition。Path是最有用的信息。JsonTextReader的LineNumber和LinePosition虽然异常里提供了但在自定义读取逻辑时可以直接访问。6.2 可视化与校验工具JSON格式化与高亮使用IDE插件如VS的JSON Viewer或在线工具让结构一目了然容易发现括号、逗号不匹配。JSON Schema验证如果API有定义Schema使用Schema验证工具可以在数据层面提前发现问题而不仅仅是语法问题。6.3 防御性编程与最佳实践始终验证输入在反序列化外部数据API响应、文件、用户输入前如果可能先进行校验。使用Try-Catch进行优雅降级反序列化代码一定要放在try-catch块中并具体捕获JsonReaderException和JsonSerializationException给用户或日志提供有意义的错误信息而不是让程序崩溃。记录原始数据在捕获异常时将出错的原始JSON字符串或片段记录到日志中。这对于复现和调试线上问题至关重要。保持模型与契约的同步当API的JSON结构发生变化时及时更新C#数据模型DTO。可以考虑使用契约测试如Pact来保证双方的一致性。考虑使用System.Text.Json对于新项目可以评估使用.NET Core 3.0引入的System.Text.Json。它性能更好默认更严格例如默认不自动转换日期有时更严格的规定反而能提前暴露数据问题。当然它的灵活性和生态目前还不如Newtonsoft.Json。处理Newtonsoft.Json的Unexpected character错误本质上是一场与数据质量和解析器期望之间的对话。最关键的步骤不是盲目尝试而是系统性地缩小排查范围从异常信息定位到具体路径检查该处的原始数据包括不可见字符对比数据与模型的类型契约最后考虑解析器的配置上下文。养成使用校验工具、记录原始数据、编写防御性代码的习惯能让你在面对这类问题时更加从容。
网站建设 高端定制 企业官网