news 2026/10/21 7:38:08

LitJson反序列化报错Double转Int64:根因剖析与五种实战修复方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LitJson反序列化报错Double转Int64:根因剖析与五种实战修复方案

做适配第三方接口或者维护老Unity项目时,这行红字应该能唤起不少人的回忆:JsonException: Can‘t assign value ‘xxx‘ (type System.Double) to type System.Int64 at LitJson.JsonMapper。尤其当项目里用的是LitJson做JSON反序列化,线上数据一更新,这种异常就冒出来了。它不是什么网络问题,也不是JSON格式非法,而是典型的“JSON里的值能解析出来,但塞不进你声明的字段类型里”。这篇文章我会把这条异常从现象到根因掰开讲透,再把我在实际项目里用过的几种靠谱修复方案按优先级列出来。遇到同类问题的朋友,可以直接照着排查、照着改。

1. 拿到异常先别慌:拆解这条报错到底在说什么

1.1 LitJson 是什么,为什么这个异常特别常见

LitJson是一个轻量级的JSON解析与生成库,在Unity客户端和老一代.NET项目中出镜率非常高。相比自带JSON工具,它支持字典、数组、嵌套对象,不需要到处打特性标记,还能通过JsonData做动态读写,很多团队在项目初期会直接引入它。代价就是它在类型转换上非常严格,一旦目标字段类型和JSON里值的实际运行时类型不匹配,就会直接抛JsonException。

这个异常最常出现在两类场景:第一类是服务端接口因为历史原因返回了带小数点形式的整数;第二类是客户端在定义数据模型时,把整数字段写成了long或int,而对方序列化时把同名字段浮点了。你做客户端时通常拿不到服务端的实际输出样本,只能靠日志里的异常信息反推,所以第一步不是猜,而是把异常文本拆开看。

1.2 从异常消息里挖出三条关键信息

LitJson的报错信息其实已经把病历写得很清楚了,只是很多人扫一眼就被JsonException吓住了。我们拿原始消息逐段看:

  • Can‘t assign value ‘xxx‘:这里面的xxx就是从JSON里解析出来的那个值,通常是一个看起来不吓人的数字,比如100或12.5。
  • (type System.Double):这个值是Double类型,说明它在LitJson内部的运行时类型不是整数,而是浮点数。
  • to type System.Int64:你写的目标字段类型是long,也就是C#里的System.Int64。
  • at LitJson.JsonMapper:抛出异常的代码位于LitJson的JsonMapper类,负责把JSON文本映射到强类型对象。

这句话翻译成人话就是:LitJson在把JSON里的一个Double类型的值写入一个long类型的字段时,发现自己没法安全完成这个赋值,于是选择直接抛异常。为什么它不帮你自动转换?因为这里存在精度和范围风险,C#的强类型机制不允许double隐式转long,LitJson默认也选择稳妥处理。

1.3 数值字面量如何被“定性”:100 为什么会变成 Double

JSON规范里其实只有一种数字类型,不像C#区分int、long、double。LitJson解析数字时,会根据字面量的写法推断具体运行时类型。如果数字写成100,大概率解析成整数类型;但如果写成100.0、100.00、1e2这种,解析结果就会变成Double。也就是说,同样数值的100,只要对端序列化框架多输出一个.0,在你这边就从整数变成浮点了。

更坑的是很多序列化框架会把数据库里的decimal、float、double类型字段统一输出成带小数点的形式。比如一个字段在文档里写着“总数”,实际JSON却是"totalCount": 100.0。你一旦把这个字段声明成long,反序列化时就会踩中这条异常。这也是为什么很多人觉得奇怪:我明明看到日志里打出来的值是100,怎么会是Double?因为100.0打印出来通常也显示成100或100,可它的运行时类型已经变了。

2. 为什么会类型不匹配:C#类型系统与LitJson赋值机制的底层逻辑

2.1 C#里double与long的转换关系

要理解这条异常,绕不开C#本身的类型转换规则。double到long不存在隐式转换,因为这种转换可能丢失小数部分,也可能在数值超过9007199254740992(2的53次方)后产生精度误差,C#语言设计上就把它列为“可能不安全”的转换。你可以手动用(long)value强制转换,也可以调用Convert.ToInt64(value),但要做的时候必须明确接受潜在精度损失。

LitJson走的是反射路线,最终要靠FieldInfo.SetValue或者类似机制把值写进字段。反射赋值不会帮你在double和long之间做强制转换,它只认类型是否兼容。不兼容时,LitJson自己有一套数值转换逻辑,尝试在数值类型之间做隐式转换;当它发现从Double到Int64无法近似安全转换,也没有注册过专门的导入器时,就直接抛出JsonException。这段逻辑不是.NET框架管的事,而是LitJson内部自己判断的。

2.2 LitJson赋值路径走到哪一步抛的

正常反序列化流程大致是:读JSON文本,按语法生成内部表示,然后通过反射把内部数据依次填充到目标对象的字段和属性上。填充阶段会根据字段类型做类型检查,比如当前值是Double而字段类型是Int64,LitJson会先查有没有为Double -> Int64注册过转换器。查不到,就尝试C#的隐式转换规则。两者都不满足,代码直接进入异常分支,抛出JsonException。

这个机制的正面效果是严格,能帮你在开发期发现契约不一致;负面效果是线上场景里一旦某个接口悄悄改了数值形态,客户端就崩给你看。理解了赋值路径,你就知道修复思路本质上只有三条路:改字段类型、让JSON里不再出现浮点形态、或者在LitJson的转换链条上挂上自定义转换器。

2.3 实际业务里最容易触发异常的三大来源

根据我处理过的项目反馈,触发这个异常的原因基本集中在三类:

  • 服务端字段类型是浮点型。比如数据库里存的是decimal或double,服务端模型也定义成浮点类型,序列化出来自然就是123.4或100.0。
  • 第三方接口文档不严谨。文档上写着“整数ID”,但实际返回的却是"id": 100.0,这类接口尤其坑人,因为你不抓原始响应很难发现。
  • 聚合统计或计算字段。后端把两个整数做除法后直接返回,比如"avg": 7.5,但客户端模型里还定义成long。

这三种情况都有一个共同点:问题不在客户端解析代码写错,而在于数据契约和消费端模型没对上。所以排查时一定要拿到原始JSON,光看异常消息里的value往往不够。

2.4 精度边界:long和double各自的地盘

既然有人会想到“那我把所有整数字段都改成double不就行了”,这里得提醒一个精度问题。long的范围是-9223372036854775808到9223372036854775807,而double虽然能表示更大的指数范围,但它精确表示整数的上限只有2^53,也就是9007199254740992。超过这个数的整数,double就会产生舍入误差。

如果业务字段是订单号、用户ID这种通常不超过2^53的数值,改成double接收问题不大;但如果碰到超过这个范围的ID、金额、计数值,无脑改成double就是在埋雷。另外浮点数的相等比较也不可靠,后续如果拿这个字段做判断,得用Math.Abs(a - b) < 1e-9之类的容差写法,不能==直接比。

3. 五种能直接落地的修复方案(含代码示例和对比)

3.1 方案一:把目标字段改成Double或Float(最低成本)

如果字段在业务上本来就允许小数,或者你明确知道数值范围不大,最简单的办法就是把字段类型从long改成double。比如原始定义是:

public class DailyReport { public long totalCount; public long avgScore; }

改成:

public class DailyReport { public double totalCount; public double avgScore; }

这样JSON里的100.0能顺利进入totalCount,7.5也能进入avgScore,异常直接消失。改完后需要注意,使用该字段的地方如果原本按整数逻辑处理,比如拿它做循环次数、数组下标、取余运算,都要先转回整数。这个方案适合字段语义本来就带小数的场景,不适合大数据量的精确主键。

3.2 方案二:用JsonMapper.RegisterImporter做全局兜底(推荐)

LitJson其实预留了类型导入器的扩展点,你可以在程序启动时给Double -> Int64注册一个转换函数。这就是JsonMapper.RegisterImporter,注册之后,反序列化过程中遇到从double到long的赋值,会直接调用你提供的逻辑。示例代码如下:

using LitJson; public static class LitJsonCompat { public static void RegisterAll() { JsonMapper.RegisterImporter<double, long>(v => Convert.ToInt64(v)); JsonMapper.RegisterImporter<double, int>(v => Convert.ToInt32(v)); JsonMapper.RegisterImporter<double, short>(v => Convert.ToInt16(v)); JsonMapper.RegisterImporter<double, decimal>(v => Convert.ToDecimal(v)); } }

注册时机越早越好,建议放在程序入口处,比如Unity的Awake、控制台项目的Main、或者框架的初始化方法里。注册一次全局生效,后续所有通过JsonMapper.ToObject<T>进行的反序列化都会遵守这套转换规则。

这里有一个细节必须提醒:Convert.ToInt64(double)和(long)double的舍入策略不一样。(long)直接截断,2.99会变成2;Convert.ToInt64(2.99)会四舍五入成3。而且Convert.ToInt64在边界上遵循银行家舍入,比如2.5会转成2,3.5会转成4,并不总是“五入”。如果你希望用截断逻辑,就写成v => (long)v,别让团队里不同人的理解产生分歧。

我还建议在importer里做一点防御:

JsonMapper.RegisterImporter<double, long>(v => { if (double.IsNaN(v) || double.IsInfinity(v)) return 0L; return Convert.ToInt64(v); });

这样即使JSON里出现NaN、Infinity这种异常浮点值,也不会把主流程带崩。

3.3 方案三:中间层接住再手动转换(最保守)

有些项目不想引入全局转换规则,担心影响其他契约。这时可以用一个“中间接收类”来兜底。先定义接收类时用宽松类型,反序列化成功后手动转成业务对象。例如把字段声明成double或LitJson自带的JsonData,再手工强转。

如果你的目标是尽量保留原始表达能力,可以把字段定义成JsonData:

using LitJson; public class RawReport { public JsonData totalCount; } // 反序列化 RawReport raw = JsonMapper.ToObject<RawReport>(json); // 手动转换 long total = Convert.ToInt64(double.Parse(raw.totalCount.ToString()));

注意这里不能直接Convert.ToInt64(raw.totalCount.ToString()),因为如果字符串是"100.0",Convert.ToInt64(string)会直接抛FormatException。正确做法是先过一遍double.Parse或Convert.ToDouble,再转成整数。这个细节我见过不少同事踩坑,代码抄过去之后又多了个新异常。

3.4 方案四:治本——推动服务端修正输出类型(根除型)

如果你能影响服务端接口,这个方案最值得推进。做法是先抓取接口原始响应,确认返回的JSON里确实是"totalCount": 100.0而不是"totalCount": 100,然后拿着证据让服务端同事把DTO里对应字段类型从decimal、float、double改成int或long。

为什么强调先抓原始响应?因为很多客户端代码里会二次处理,比如有人用别的解析库先把JSON转成中间对象,再打印出来看就成了100,误导排查方向。正确的证据链应该是:直接用网络层打印原始响应文本,或者用文本编辑器看抓包内容。一旦确认服务端输出了.0,这个问题就好解决了。契约规范里也可以增加一条约定:语义为整数的字段,序列化结果不允许出现小数点。

3.5 方案五:字符串字段接收后按需转换(最保险但最繁琐)

如果对接的是完全不可控的外部接口,或者对方接口一天变三次,那最保险的做法是先用string接收,业务层再做转换。比如:

public class SafeData { public string totalCount; public string message; } // 使用的时候 long total = Convert.ToInt64(double.Parse(safe.totalCount));

这样无论对方返回100、100.0还是"100",字符串字段都不会抛JsonException。代价是每个数字字段都要多写一次转换,代码量变大,而且需要明确约定“解析失败时怎么处理”。这个方案适合小范围使用,比如只针对几个问题字段,不建议整个数据模型全部用string,否则维护成本会失控。

3.6 方案对比总览

方案改造成本风险点适用场景
字段改double低大整数精度丢失,浮点比较问题字段语义本来就是小数,数值范围可控
RegisterImporter中全局生效,舍入策略需统一历史接口多,想一次性兜底
中间层手动转换中每处都要手动处理,容易漏转字段少、结构不稳定、不想影响全局
推动服务端修正低(客户端侧)依赖对方配合,联调周期不定接口协议可控,有契约评审
string接收低到中代码繁琐,后续使用要反复转换外部接口混乱,字段量少

4. 实操记录:从一个最小复现工程到彻底修复

4.1 最小复现工程搭建

为了验证解决方案,最直接的办法就是复现。这里我写一个最简控制台例子,模拟服务端返回了带小数点形式的JSON:

using System; using LitJson; public class DailyReport { public long totalCount; public long onlineCount; } public class Program { public static void Main() { string json = "{\"totalCount\": 100, \"onlineCount\": 100.0}"; DailyReport report = JsonMapper.ToObject<DailyReport>(json); Console.WriteLine(report.totalCount); } }

执行时,totalCount能正常解析,但onlineCount会触发JsonException。因为JSON里的100.0被LitJson解析成Double,而字段类型是Int64。

4.2 用RegisterImporter修复的完整步骤

第一步,新建一个静态类,集中注册所有项目需要的转换器:

public static class LitJsonCompat { public static void RegisterAll() { JsonMapper.RegisterImporter<double, long>(v => { if (double.IsNaN(v) || double.IsInfinity(v)) return 0L; return Convert.ToInt64(v); }); JsonMapper.RegisterImporter<double, int>(v => { if (double.IsNaN(v) || double.IsInfinity(v)) return 0; return Convert.ToInt32(v); }); } }

第二步,在程序入口调用注册方法:

public static void Main() { LitJsonCompat.RegisterAll(); string json = "{\"totalCount\": 100, \"onlineCount\": 100.0}"; DailyReport report = JsonMapper.ToObject<DailyReport>(json); Console.WriteLine(report.onlineCount); }

第三步,重新运行,确认异常消失,onlineCount被转换成100。如果后续接口返回了98.5这样的真正小数,它也会被四舍五入成整数,这个取舍要在业务层面确认可接受。

4.3 验证要点与回归测试

修复不是“能跑就行”。我建议至少用下面几组JSON做验证,防止其他边界问题:

  • 纯整数:"score": 98,验证普通整数正常。
  • 带一位小数:"score": 98.0,验证本次异常场景。
  • 真正的小数:"score": 98.6,验证转换策略是否符合预期。
  • 科学计数法:"score": 9.86E1,验证指数形式不会引发新异常。
  • 极端值:"score": 9.22E18,观察是否溢出long范围。
  • 字符串数字:"score": "98",确认string到long不在importer范围内时,会报另一种异常。

如果目标是生产级,把这些case写成单元测试,在CI里跑一遍,后续接口再变动时能快速发现回归。

4.4 性能实测与注意事项

有人担心注册importer会影响性能。实测中,RegisterImporter的委托调用开销相比反射和JSON解析本身小到可以忽略,真正耗时还是在解析和对象构造上。要注意的是不要在热更新或业务循环里反复注册同一个转换器,尽量在启动阶段一次性注册完。不同转换器之间如果逻辑有冲突,后注册的会覆盖先注册的,所以团队里要约定好统一入口,避免两处代码各注册一套规则。

如果项目的LitJson是源码引入而不是DLL,你还可以直接在JsonMapper.cs的数值转换分支里做手动兼容。但这样一来库的代码就和业务耦合了,升级Lib版本时容易忘记合并,我一般不太推荐,除非你是那个库的唯一维护者。

5. 常见变体与排查技巧实录

5.1 这个异常最常见的五种变体

这个异常不只是Double -> Int64一种形态,把目标字段改成别的整数类型还会出现其他变体。整理了一张表,方便对照:

异常消息里的类型组合常见原因建议处理
Double -> Int64数值带小数点或科学计数法RegisterImporter或改double
Double -> Int32字段声明成int,数值是浮点同上,注册double到int
String -> Int64JSON里是"123"带引号服务端不要引号,或字段用string再接
JsonData -> Int64字段类型不明确,LitJson包装成JsonData中间层手动转换
Null -> Int64JSON里是null,字段是值类型字段改成可空类型long?或做默认值处理

5.2 排查动作:怎么在日志里快速锁定罪魁祸首

我排查这类问题通常三步走。第一步,把抛出异常的json原文和type一起打出来,不要只打异常消息。第二步,搜索异常消息里的value值,确认它在JSON中的位置。第三步,打开目标类的定义,逐个数字字段核对类型。

为了省事,可以写一个包装方法,把原始JSON信息带在日志里:

public static T SafeToObject<T>(string json) { try { return JsonMapper.ToObject<T>(json); } catch (JsonException ex) { UnityEngine.Debug.LogError($"JSON解析失败,目标类型: {typeof(T).Name}, 原始JSON: {json}"); throw; } }

这样再看到异常时,上下文信息齐全,不用再翻网络层日志。

5.3 多个JSON库混用的坑

一个项目里同时存在LitJson和其他序列化库的情况并不少见,比如Unity自带工具、LitJson、第三方强类型库共存。混用的最大问题是同一份JSON在不同库里的行为不一致。有些库自动把100.0转给long字段,LitJson却会抛异常;有些库对Long类型字段赋值宽松,LitJson又很严格。所以排查时要先搞清楚当前代码到底走的哪个库、哪条解析路径,别拿着一份LibA的行为去推理LibB的结果。

5.4 预防习惯与管理规范

要我总结预防经验,核心只有一条:数据契约测试。每次接入新接口时,拿到一份真实响应样本,把它保存成测试资源,跑一遍反序列化用例。这个动作成本很低,但能挡住大量线上崩溃。其次,DTO定义阶段就要对整数字段类型达成共识,遇到统计类、计算类字段时优先考虑double或decimal,不要默认写long。最后,代码评审时盯一下RegisterImporter的舍入策略,Convert和(long)的区别必须写清楚,免得大家各写各的。

最后分享一个小习惯。我处理过一个对接需求,对方接口文档里写“库存数量为整数”,实际返回却是"stock": 100.0,导致客户端一整天都在报这个异常。后来我把库存字段改成double,又在服务端流程上推动对方修了序列化配置。从那以后,我接任何接口都会先问一句:整数字段能保证不带小数点吗?然后把这条写进对接清单。别小看这个细节,它能帮你省下后面无数个“为什么线上又崩了”的下午。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 7:38:07

FPGA驱动DS1302实现掉电走时万年历设计

简介&#xff1a;这是一套完整的基于AX301开发板与DS1302芯片的万年历FPGA工程&#xff0c;面向数字逻辑、RTC读写与FPGA综合开发学习者。工程覆盖年、月、日、时、分、秒、星期的读取与显示&#xff0c;结合四键交互实现校准&#xff1a;key1进入修改并控制闪烁位&#xff0c;…

作者头像 李华
网站建设 2026/10/21 7:38:00

杰理AC696N开发板Windows开发环境搭建完整指南

很多人拿到一块新开发板&#xff0c;第一反应都是先插USB、找资料、装IDE。我拿到AC6966B开发板的时候也差不多&#xff0c;但实际走了一遍才发现&#xff0c;这套AC696N开发环境并不是一个安装包能搞定的&#xff0c;它由固件工程、编译工具链、下载工具和调试串口四部分拼装而…

作者头像 李华
网站建设 2026/10/21 7:37:58

高质量博文创作:从完整信息输入到技术内容落地

我注意到本次输入的内容为空——没有项目标题、没有项目正文、没有关键词&#xff0c;也没有摘要描述或网络热词素材。我无法凭空编造一篇博文&#xff1a;那样既违背了"忠于原料、合理演绎"的核心原则&#xff0c;也无法保证内容贴合你实际想表达的主题。请提供完整…

作者头像 李华
网站建设 2026/10/21 7:36:44

智慧能源节能监管平台落地指南:从数据采集到指标闭环

简介&#xff1a;面向政府数字化与能源管理领域&#xff0c;这份326页的智慧能源数字化节能监管大数据平台建设方案&#xff0c;系统梳理了市级政府从用能现状诊断到平台总体设计的完整路径。方案聚焦用电、用水、用能安全及后勤能源管理四大现状&#xff0c;结合国家与行业标准…

作者头像 李华
网站建设 2026/10/21 7:36:39

PostgreSQL 12 分区表 + postgres_fdw 分库分表实战:原理、配置与调优

做后端和数据库的&#xff0c;早晚会遇到一个场景&#xff1a;单张表的数据量到了几千万甚至上亿&#xff0c;索引越建越大&#xff0c;写并发上不去&#xff0c;原本毫秒级返回的SQL开始慢慢吞吞。我最初的处理方式是应用层分表&#xff0c;在SQL里拼接表名&#xff0c;路由逻…

作者头像 李华
网站建设 2026/10/21 7:35:59

从零配置Java开发环境:JDK、环境变量与IDEA安装全攻略

1. 先把版本选对&#xff1a;JDK下载前的三个关键决定Java新手入坑的第一道坎&#xff0c;往往不是写代码&#xff0c;而是把环境搭起来。装完IDEA装JDK&#xff0c;配完环境变量又发现中文插件找不到&#xff0c;这一整套流程要是没人带着捋一遍&#xff0c;折腾半小时起步&am…

作者头像 李华