
干了好几年C#开发我对“JSON→C# Entities”这件事感触特别深。早些年对接第三方接口最烦的就是对方丢过来一大段嵌套JSON、几十个字段让我自己撸实体类。那时候纯手动敲字段名靠肉眼核对类型靠猜经常因为少个下划线或者类型写错联调时被各种反序列化异常教做人。后来把这套流程沉淀成固定套路从拿到JSON到生成可用实体类基本控制在5分钟以内今天就用一个完整案例聊聊我是怎么做的。这篇内容适合谁不管你是在用WinForm做工具类软件、搞上位机对接设备数据还是日常写Web API、做后端接口联调只要经常跟JSON打交道都会有帮助。我会从生成路径、生成后整理、常见报错排查讲到复杂JSON场景全程用可复现的案例说话确保看完不是只会点按钮而是能理解每一步背后的原因。1. 先想清楚都说“实体类生成很简单”到底难在哪1.1 手动敲实体类为什么会翻车很多刚入行的朋友觉得实体类就是照着JSON抄一遍属性没什么技术含量。但真去抄一遍就发现手工操作在字段量上来之后特别容易出问题。最典型的是类型判断。接口返回里常见这种数字id: 1234567890123456789如果只看了前面几位随手定义成int等数据量一大ID超出int上限反序列化直接抛异常。又比如金额字段没有引号却带小数有人图省事写float结果金额精度丢失报表对不上账。这类问题不是看一眼JSON就能发现的得观察值域、精度和上下文。第二个坑是嵌套层级。真实接口很少有扁平结构data里头套itemsitems里再套skuList手写的时候稍微一走神就把内层类的属性定义到外层去了。编译一时半会儿不报错运行时路径对不上排查起来反而更费劲。还有一个几乎人人踩过的坑JSON里的键是order_idC#里按PascalCase规范写了OrderId直接用默认规则反序列化发现属性一直是默认值。你说它报错吧它不报就是数据是空的这种问题比报错还阴间因为很多时候到业务层才发现。1.2 自动生成到底解决了什么问题自动生成实体类的核心价值不是替你把代码写好而是把“机械劳力”和“设计决策”剥离开。看到一段JSON真正需要你思考的永远是这个字段在业务上是什么含义它应该用什么CLR类型承载哪些是可空字段哪个子对象要单独复用而“照着data.items[0].name写一个public string Name { get; set; }”这种操作纯粹是体力活交给工具反而更不容易出错。所以我的定位很明确JSON→C# Entities工具负责“翻译”我负责“审校”。工具把JSON的结构翻译成C#类型骨架我再根据业务语义修正类型、补齐特性、调整组织结构。这样既快又稳不会因为字段太多漏掉什么也不会因为完全迷信工具导致契约设计失控。2. 动手前花30秒先把JSON结构看明白2.1 先看开头是“{”还是“[”很多人拿到JSON就急着找工具粘贴我建议先花30秒人工判断三个基础问题这30秒能省下后面一堆返工。第一个问题最简单也最关键JSON根节点是对象还是数组开头是{说明这个接口返回的是单条数据对应C#里的实体类开头是[说明是多条数据C#里要包一层ListT或者至少用集合类型接收。如果遇到接口设计不规整的比如data字段有时是对象、有时是数组也别慌先按对象建模后面用自定义转换器或者JsonElement兜底。2.2 再看嵌套层级和键命名风格接下来扫一遍整体层级。我习惯从最外层开始往内数像剥洋葱一样把层级列出来根对象→data→items→skuList。心里有了一张“层级地图”之后再动手生成就有底了。同时也要留意键的命名风格。JSON常见的命名风格有这么几种小驼峰orderId、大驼峰OrderId、下划线order_id、全小写orderid。C#这边约定俗成用PascalCase大驼峰命名字段如果两边风格不一致或者干脆没有规则光靠工具生成出的属性名不一定符合要求强行用又会显得代码很乱这里就需要后面说的显式映射来解决。2.3 识别字段真实类型数字也可能不是int这一步是新手最陌生的。看到total_amount: 99.50注意值被引号包着不管它看起来多像数字在JSON世界里它都是字符串。实体类里最安全的做法是定义成string需要计算时再转成decimal。这样既能保证反序列化不失败也不会丢精度。再比如create_time: 1700000000从数字位数和上下文判断大概率是Unix时间戳单位是秒。此时实体类里定义成long最合理要用DateTime就自己写个转换逻辑千万别图省事直接定义成string否则后面每次取出来都要处理一遍很啰嗦。2.4 数据安全提醒敏感数据先脱敏这个环节单独提出来多说一句。现在很多快速生成实体的路径是在线工具或者AI助手确实方便但如果你贴的是一份包含真实手机号、身份证号、token或者内部业务数据的线上JSON就是在把敏感信息送给第三方平台。我自己的习惯是在线转换之前先手动把敏感字段的值改成xxx、0、空对象这类占位值结构保持原样。脱敏用的时间不超过一分钟但能避免很多不必要的麻烦。3. 5分钟实操一份JSON跑通三种生成路径3.1 先准备一份测试JSON为了演示真实流程我构造了一份订单接口返回的JSON尽量覆盖常见情况{ code: 0, message: ok, data: { order_id: 20240001, customer_name: 张三, total_amount: 199.00, status: 3, create_time: 1700000000, items: [ { product_id: 1001, product_name: 商品A, price: 99.00, count: 1 }, { product_id: 1002, product_name: 商品B, price: 100.00, count: 1 } ] } }这份JSON包含了根对象、嵌套对象、数组嵌套、字符串形式的金额、下划线命名、时间戳数字基本覆盖了日常联调会遇到的大部分情况。下面用三种常见路径来生成实体类。3.2 路径一Visual Studio自带“选择性粘贴为类”如果你用的是Visual Studio最快的一条路是直接复制JSON然后在C#文件里依次点编辑 → 选择性粘贴 → 将 JSON 粘贴为类。这个功能不需要联网也不用把数据贴给第三方安全性是最好的。粘贴后会生成类似下面的代码不同版本略有差异但骨架基本一致public class Rootobject { public int Code { get; set; } public string Message { get; set; } public Data Data { get; set; } } public class Data { public string Order_id { get; set; } public string Customer_name { get; set; } public string Total_amount { get; set; } public int Status { get; set; } public long Create_time { get; set; } public ListItem Items { get; set; } } public class Item { public int Product_id { get; set; } public string Product_name { get; set; } public string Price { get; set; } public int Count { get; set; } }这里有个需要特别留意的点VS自带的转换并不会帮你把order_id自动映射成OrderId生成的属性名跟JSON里的键几乎一模一样。所以我会把这段代码当成“第一版草稿”接下来要去手动做三件事把下划线属性名改成PascalCase、补上序列化特性、检查status这种数字字段是否要用枚举。这一步就是“5分钟流程”里最花时间但最值得的部分。3.3 路径二用quicktype或json2csharp快速出带特性的类如果你不想手动加JsonProperty可以用在线的quicktypeapp.quicktype.io或者json2csharp这类工具。操作步骤很固定左边粘贴JSON语言选C#然后在选项里确认属性命名风格和是否生成JSON特性。quicktype里一般可以勾选让属性名输出为PascalCase同时自动为每个属性生成[JsonProperty(原key)]这就是我推荐的方式。生成结果大致长这样public partial class Root { [JsonProperty(code)] public long Code { get; set; } [JsonProperty(message)] public string Message { get; set; } [JsonProperty(data)] public Data Data { get; set; } } public partial class Data { [JsonProperty(order_id)] public string OrderId { get; set; } [JsonProperty(customer_name)] public string CustomerName { get; set; } [JsonProperty(total_amount)] public string TotalAmount { get; set; } [JsonProperty(status)] public long Status { get; set; } [JsonProperty(create_time)] public long CreateTime { get; set; } [JsonProperty(items)] public ListItem Items { get; set; } }对比VS路径这种方式生成的代码更“C#风格”属性名是PascalCase同时保留了下划线键名到属性的映射反序列化时不会因为名字对不上丢字段。需要注意在线工具生成的一些辅助构造函数和FromJson方法通常用不上我一般只复制类定义部分。另外工具默认把所有数字都生成为long如果字段值域比较小我会根据自己的判断手动改成int。3.4 路径三用AI助手辅助生成时的检查清单这两年我偶尔也会用AI助手直接生成实体类。操作更灵活能顺便让它帮我把create_time转成DateTime或者把status转成枚举非常方便。但这里有个很实际的提醒AI生成代码的临场发挥空间比较大你给它的JSON如果字段比较多它可能在中途漏掉某些字段或者自己“理解”错了类型。我使用AI辅助时的固定提示词框架大概是这样的把JSON粘贴进去然后要求“把这段JSON转换为C#实体类属性使用PascalCase命名保留与JSON键名对应的JsonProperty特性金额字段用string类型时间戳用long类型不要修改任何字段名”。明确约束之后生成结果基本可用。但不管用哪种AI工具之后都必须人工对一遍字段数量。最简单的办法是数一下原JSON有多少个叶子字段再数一下生成出来的类里有多少个属性。只要数量对得上结构多半就没问题。4. 生成之后先别急着跑属性整理与类型校准4.1 字符串数字与decimal到底怎么选上一节的JSON里total_amount是199.00。有人可能会问为什么工具生成的结果里它是string而不是decimal这正是因为原JSON里这个值带了引号。如果把实体类属性定义成decimal反序列化时System.Text.Json在默认配置下会直接抛异常因为字符串不能隐式转成数字。实际业务中很多财务系统就是故意用字符串传金额的目的是保留原始格式避免二进制浮点误差。所以用string承载完全没有问题计算时再通过decimal.Parse或decimal.TryParse转换就行。需要注意的是转换时的culture问题建议都用decimal.Parse(value, CultureInfo.InvariantCulture)否则在一些系统区域设置下小数点解析会翻车。反过来如果接口返回的是不带引号的total_amount: 199.00那实体类里就应该定义成decimal。这里再次强调不推荐用double或float承载金额0.1加0.2这种经典精度问题放到真实订单金额上就是事故。4.2 命名映射JsonProperty与JsonPropertyName的区别很多新手搞不清楚JsonProperty和JsonPropertyName的区别其实跟序列化库有关。Newtonsoft.Json用的是[JsonProperty(order_id)]System.Text.Json用的是[JsonPropertyName(order_id)]。网络上的很多旧代码示例都是Newtonsoft风格的但如果你新建的项目用的是System.Text.Json复制过来就不生效。我平时写实体类如果项目明确用System.Text.Json就写成public class Data { [JsonPropertyName(order_id)] public string OrderId { get; set; } }如果项目还在用Newtonsoft.Json很多老项目、一些工具类库仍在使用则写public class Data { [JsonProperty(order_id)] public string OrderId { get; set; } }在同一个类里混用两套特性虽然不至于编译报错但会让人困惑而且也没有必要。建议一个项目统一用一种序列化库不要在实体类上两边都打。4.3 可空类型与未知字段兜底生成后的实体类建议把可能存在null的引用类型属性标记清楚。比如customer_name如果接口在特殊情况下可能不返回这个字段属性定义成string?会比string更准确。这里说的可空不是运行时能否赋null而是表达“这个字段在业务语义上允许为空”对后续代码审查和调用方理解契约都有好处。如果遇到接口返回字段比文档多或者后续版本会追加字段的情况还有个很实用的兜底技巧在类里加一个JsonExtensionData字典。Newtonsoft.Json和System.Text.Json都支持代码类似这样public class Data { [JsonPropertyName(order_id)] public string OrderId { get; set; } [JsonExtensionData] public Dictionarystring, System.Text.Json.JsonElement ExtraData { get; set; } }这样即使JSON里带了实体类没定义的字段也不会反序列化报错多余字段会被收进ExtraData。等到前端要展示这些未知字段时再动态去取不至于直接丢数据。4.4 内部类到底放在哪里用工具生成时多个类经常出现在同一个文件里甚至类名直接叫Rootobject、Data这样非常通用的名字。我的习惯是拿到生成结果后马上重命名把Rootobject改成有业务含义的名字比如OrderResponse把内部的Data改成OrderData避免以后和其他接口的数据类重名。对于嵌套的Item如果只在订单接口里使用放在同一个文件里完全没问题但如果你发现项目里有多个接口都返回类似的“商品明细”结构就值得抽成独立文件独立类方便复用。抽类的时候顺便用partial修饰将来补充自定义属性或方法也不需要频繁改动原始文件。5. 真实联调里最常见的反序列化报错速查5.1 Failed to deserialize疑似找不到某字段不少人在ASP.NET Core Minimal API场景下会遇到这样一段异常提示Failed to deserialize the JSON body into the target type: ... Missing field xxx这句话的常见场景是客户端或上游服务传来的JSON里某个字段缺失或字段名对不上而且目标的DTO配置成了严格模式。排查路径一般是这样先看异常信息里的Path或Field指向哪个字段再回到JSON源数据确认是不是真的有这个键。如果没有说明上游漏发了如果有但名字不匹配就要检查实体类属性上的序列化特性是不是写错或没写。比如C#属性叫OrderId上游JSON键是order_idSystem.Text.Json默认大小写敏感且不会自动处理下划线就必须通过[JsonPropertyName(order_id)]来指定或者在配置中约定命名策略。另外如果不想因为某个可选字段缺失影响整条反序列化可以在配置里把匹配规则放宽。Newtonsoft.Json可以设置MissingMemberHandling.IgnoreSystem.Text.Json则要结合.NET 8后的UnmappedMemberHandling配置。但我的建议是契约字段是接口的核心宁可在联调阶段暴露缺失也不要默默吞掉否则到了生产环境数据对不上更可怕。5.2 JSON value of type String cannot be converted to Int64这个报错几乎是“金额字符串”或“数字字符串”场景的标配。JSON里的count: 3你在实体类里写成了public int Count { get; set; }反序列化时就会抛出类似“The JSON value could not be converted to System.Int64”的异常。解决办法无非两条要么把实体类属性改成string保留原值在业务代码里转要么写一个自定义JsonConverter把字符串数字当成数字转换。我的建议是如果是上游接口设计如此就用string接不要强行写转换器因为后面可能还会出现3.0这种带小数的字符串到时候转换器又得打补丁。如果报错是数字和数字之间的精度问题比如JSON里是3.9你定义了int那也得结合业务判断是不是字段类型本身选错了。一些打分排序字段确实可能返回整数但保险起见我会用double或decimal承载避免上游某天返回小数直接全线崩溃。5.3 属性有值但调用方读到默认值这个属于“不报错但数据不对”的经典情况。如果属性是OrderId而JSON键是order_id在System.Text.Json默认配置下反序列化时找不到对应属性但又不会直接报错结果就是OrderId一直是null或0。排查时先确认你是否忘了加[JsonPropertyName]再看全局配置里有没有设置PropertyNamingPolicy JsonNamingPolicy.CamelCase。CamelCase策略只能解决orderId映射OrderId的情况解决不了下划线风格。如果项目里大量对接第三方下划线JSON我建议在反序列化配置里做一层自定义命名策略或者老实写映射特性。另外还有一个非常隐蔽的问题JsonProperty写到了错误的属性上。复制粘贴时手滑把A属性的特性贴到B属性头上这类错误代码编译不报错只能靠写一个包含所有字段的测试JSON来验证。6. 复杂JSON场景的实体类设计套路6.1 data字段不停变化用JsonElement做中间态实际接口里经常有一种痛点响应码是0的时候data是一个订单对象响应码是别的值时data可能是一个错误提示字符串。对这类字段类型不固定的JSON强行在实体类里写死Data Data是不合适的。我的做法是先用JsonElement接收这个字段public class Response { [JsonPropertyName(code)] public int Code { get; set; } [JsonPropertyName(data)] public JsonElement Data { get; set; } }拿到JsonElement后再根据code判断是走对象解析还是错误信息处理。这样做的优点是延迟决策先保证反序列化不失败再在业务层面对不同形态的数据做分叉处理。缺点是不够强类型但面对不稳定的上游接口这反而是最稳的缓冲方案。6.2 数组字段的命名与强类型集合选择如果JSON里出现了数组很多工具默认生成ListT这个没问题。但有些时候上游会把空数组返回成null如果你的属性定义成ListItem并且没有做null防护一调用就空引用。建议所有集合类型的属性都初始化成空集合或者在构造函数里Items new ListItem()。更符合现代写法的做法是只在模型层承载数据不在构造函数里塞逻辑那就让调用方在访问前用?.防护。下划线键名的数组嵌套在生成时也容易乱。比如items数组里每一项的键是product_id生成后如果工具没有映射特性反序列化到ProductId就失败。所以生成后第一件事就是检查集合元素类里的每个属性有没有正确的序列化特性。6.3 状态字段什么时候适合转枚举看到status: 3这种数字很多人第一反应是定义一个枚举比如OrderStatus让代码可读性更好。这个思路没错但要注意一个前提上游返回的枚举值范围稳定不会随意新增。我踩过的坑是上游在某个灰度版本里新增了一个status: 8而我的枚举里根本没定义8System.Text.Json在默认配置下处理未知枚举值时会抛异常导致整个解析失败。解决方案有三种一是不用枚举直接用int只读场景下配合switch表达式解释含义二是枚举加一个[EnumMember]反序列化仍然会失败三是写一个自定义JsonConverter遇到未知数字时映射到Unknown 0兜底。具体选哪个取决于你对上游稳定性的判断没有绝对答案。但对于需要长期维护的接口我倾向于用int接收宁可代码稍微不“优雅”也要保证容错。7. 一些我踩过的坑和操作习惯最后分享几个我自己实践下来的固定习惯不一定适合所有项目但至少能帮你少走弯路。第一个习惯是生成完实体类之后马上写一个“最小反序列化测试”。不需要完整的单元测试项目在本地控制台或者测试方法里把示例JSON喂进去看关键字段能不能对上。这一步看着费时间但能一次性发现类型错误、命名映射错误、缺失字段比上了生产环境再排查高效得多。第二个习惯是JSON示例里一定要包含边界值。只测正常数据不够我会刻意在样例里放上null、空数组、超长字符串、带小数的金额实体类能不能抗住这些情况往往才是线上会不会出事的关键。如果上游字段允许为空但实体类属性写成了不可空类型反序列化时一旦遇到null就容易出问题。第三个习惯是保持实体类“小而专”。一个接口一套DTO不要图省事把A接口的返回字段全塞进B接口的实体类里。经常看到有人为了少写几个类把十几个可能为空的字段堆在一个总类里表面看是省事实际上调用方根本不知道哪些字段一定有效代码的可读性和安全性都会下降。工具能帮你把JSON变成C#实体类但实体类在业务里的边界还是得人来定。