别手写 JSON 解析了:UE5 里 Struct 和 JSON 到底怎么互相转
很多 UE5 开发者在做网络通信、存档系统、配置表加载或者 Web API 对接时,都遇到过同一个问题:客户端拿到的是一段 JSON 字符串,而游戏逻辑里需要的是一个结构体对象;反过来,结构体对象要发给服务器,又得先序列化成 JSON。
如果项目里只有一两个字段,手动拼字符串还能忍。但一旦结构体多了、字段嵌套了、数组出现了,手写解析代码就成了一场灾难:每个字段对不上、类型写错、加一个字段要改三处地方。很多人在这一步放弃了 UE5 自带的 JSON,转而引入第三方库,结果又遇到编译问题、插件冲突、跨平台兼容性坑。
这篇文章要讲的核心是一个很实用的方向:在 UE5 中把 Struct 与 JSON 做双向转换,并围绕一个名为 StructJsonString 的插件思路做完整的原理解析与代码演示。读完你会得到一个明确判断:这类转换工作完全可以在项目里自动化,不值得手写,也不值得引入重型库;关键是选对方案,并理解其底层机制。
文章会从痛点出发,讲清楚 Struct、JSON、反射系统三者的关系,然后给出完整可复制的蓝图与 C++ 实现方式,最后补充常见坑和工程建议。
1. 为什么 UE5 开发者绕不开 Struct 与 JSON 的互相转换
先看几个真实开发场景。
第一个场景是登录系统。客户端请求服务器,服务器返回一个 JSON:
{ "code": 0, "message": "success", "data": { "userId": "10001", "nickname": "UE5玩家", "level": 30, "vipLevel": 3 } }如果你的代码里需要读取 nickname 和 level,最笨的办法是用 UE5 的 FJsonObjectConverter 或者 FJsonObject 一层层解析。这非常繁琐,尤其是当 data 结构复杂时,你得写一堆 GetStringField、GetIntegerField、GetObjectField,还可能漏判字段是否存在。
第二个场景是存档系统。你有一个战斗属性结构体,需要把它写入本地存档文件,下次启动时再读回来。如果手写序列化,那么你每新增一个属性,存档的读写逻辑都要同步改一遍,极易出错。
第三个场景是配置表。策划配置了一个 JSON 文件,里面的内容是怪物或技能参数,你需要把它加载到结构体数组里。字段一旦多起来,手写解析的代码量基本是爆炸式的。
这些场景共同指向一个需求:对象的字段名与 JSON 键名自动对应,类型自动转换,嵌套结构自动处理。这正是 Struct 与 JSON 双向转换要做的事情。
先说结论:在 UE5 里,Struct 转 JSON 和 JSON 转 Struct 都不需要手写字段级代码,因为引擎的反射系统早已提供了通用能力。问题在于很多开发者没有完整理解这些内置能力,或者不了解插件封装之后的使用姿势,导致走了弯路。
2. Struct 与 JSON 双向转换的核心原理
要搞清楚转换,必须先理解 UE5 里的三个核心概念:Struct、反射系统、JSON 数据结构。
2.1 Struct 在 UE5 中的角色
Struct,即结构体,是 UE5 中非常常用的数据类型。它和 Actor 不同,并不是一个独立的世界物体,而是一组数据的集合。比如:
USTRUCT(BlueprintType) struct FPlayerInfo { GENERATED_BODY() UPROPERTY(BlueprintReadOnly) FString UserId; UPROPERTY(BlueprintReadOnly) FString Nickname; UPROPERTY(BlueprintReadOnly) int32 Level; UPROPERTY(BlueprintReadOnly) int32 VipLevel; };结构体在蓝图里也可以定义,适合做数据打包、函数参数传递、存档和网络传输的数据载体。
2.2 反射系统为什么关键
UE5 中的 UPROPERTY、UCLASS、USTRUCT 这套宏,不仅仅是给编辑器识别用的。它们会生成一份运行时元数据,记录每个属性的名称、类型、访问权限等信息,这就是反射系统。
有了反射系统,引擎才可能在“不知道具体类型”的情况下,遍历一个结构体的所有属性。这才是 Struct 与 JSON 双向转换能实现的地基。
如果没有反射系统,想把一个结构体变成 JSON,只能通过手写每个字段的操作,或者靠代码生成工具在编译期硬编码。UE5 内置了反射,所以我们可以写一个通用函数,不管结构体长什么样,都能递归遍历出所有属性,并映射到 JSON 的键值对。
2.3 JSON 的本质
JSON 本质上就是一套嵌套的键值对和数组结构,对应到 UE5 里主要是 TMap、TArray、FJsonObject。
Struct 转 JSON,就是把 FPlayerInfo 这样的结构体转换成 FJsonObject 再输出为 FString。JSON 转 Struct,则是把 FString 解析成 FJsonObject,再回填到结构体实例里。
2.4 内置能力 FJsonObjectConverter
UE5 内置了一个很关键的类:FJsonObjectConverter。它利用反射系统提供了结构体和 FJsonObject 互转的能力。核心接口包括:
UStructToJsonObject:把 UStruct 转换成 FJsonObject。JsonObjectToUStruct:把 FJsonObject 转换成 UStruct。UStructToJsonObjectString:把 UStruct 直接转换成 JSON 字符串。
这意味着引擎本身已经解决了大部分转换问题。那插件 StructJsonString 存在的意义是什么?它解决的是易用性、蓝图暴露、数组嵌套处理、调试体验和错误处理这些工程层面的问题。
核心结论:Struct 与 JSON 的双向转换,重点不是自己写算法,而是理解 UE5 反射系统,并选择一个封装好、可直接用的方案。3. 什么样的项目真正需要这个能力
不是所有项目都需要在 Struct 与 JSON 之间频繁转换。如果只是本地写死几个变量,无脑引入插件反而增加复杂度。但从真实项目看,以下四类需求非常典型:
3.1 网络通信与 Web API 对接
UE5 客户端作为 HTTP 客户端请求服务器接口,返回 JSON 数据,如公告、玩家信息、排行榜、商城列表。用双向转换能力后,返回的 JSON 能直接变成结构体数组,逻辑层不再处理 JSON 细节。
3.2 存档系统
游戏存档本质上是把内存中的结构体序列化为字符串,保存到磁盘或云服务器。读取时再反序列化为结构体。这个场景对转换效率和类型安全性要求很高。
3.3 配置与热更数据
策划的数值配置以 JSON 下发,客户端读取后转为结构体。字段变化的维护成本,直接取决于转换方案是否自动。手写解析会让每次配置调整都变成一次代码修改。
3.4 多人联机的数据传输
虽然 UE5 自带 RPC 和属性复制,但在某些自定义网络协议、消息队列、WebSocket 扩展场景下,依然需要把 Struct 转成 JSON 字符串发送,再从 JSON 恢复为 Struct。
所以,如果你正在做以上任意一类项目,这篇内容就值得读完。
4. 环境准备与图纸:先理解 StructJsonString 插件的能力边界
关于具体插件的版本与安装方式,请以项目实际情况为准。本文重点演示通用思路:用引擎内置能力和一套自定义封装,实现“一行代码完成 Struct 与 JSON 双向转换”。
在动手之前,先明确插件或者封装模块需要具备的能力清单:
| 能力 | 说明 |
|---|---|
| Struct 转 JSON 字符串 | 支持嵌套结构体、数组、Map、枚举、基本类型 |
| JSON 字符串转 Struct | 支持缺失字段容错、类型转换容错 |
| 蓝图节点 | 让蓝图也能直接使用,不局限于 C++ |
| 数组支持 | 结构体数组能整体转成 JSON 数组 |
| 调试输出 | 转换失败时有明确日志提示 |
如果某个插件声称支持双向转换,但不支持嵌套结构体或数组,那基本都是半成品,实际项目里并不好用。
4.1 创建一个测试 C++ 项目
我们可以新建一个基础的 UE5 C++ 项目。如果你使用蓝图项目,也可以稍后通过编辑器模块启用 C++。示例环境建议:
- 操作系统:Windows 10/11 或 macOS。
- UE 版本:UE 5.0 及以上即可,思路通用。
- 编译工具:Visual Studio(Windows)或 Xcode(macOS)。
- 构建方式:标准 UnrealBuildTool(UBT)。
4.2 定义示例结构体
在项目 Source 目录中,新建一个头文件,例如JsonConvertTestTypes.h,并定义两个结构体:一个基础角色信息、一个包含嵌套与数组的复杂结构体。
// 文件路径:Source/YourProject/Public/JsonConvertTestTypes.h #pragma once #include "CoreMinimal.h" #include "JsonConvertTestTypes.generated.h" USTRUCT(BlueprintType) struct FPlayerInfo { GENERATED_BODY() UPROPERTY(BlueprintReadOnly) FString UserId; UPROPERTY(BlueprintReadOnly) FString Nickname; UPROPERTY(BlueprintReadOnly) int32 Level = 1; UPROPERTY(BlueprintReadOnly) int32 VipLevel = 0; }; USTRUCT(BlueprintType) struct FPlayerTeamInfo { GENERATED_BODY() UPROPERTY(BlueprintReadOnly) FString TeamName; UPROPERTY(BlueprintReadOnly) TArray<FPlayerInfo> Members; };这里的 FPlayerTeamInfo 包含了一个结构体数组,后续会用它验证复杂转换能力。
5. 手写一个通用转换封装:StructJsonString 的迷你实现
为了让你不依赖某个具体插件也能理解原理,这里直接写一个迷你版封装。它本质上就是基于 FJsonObjectConverter 的二次封装,但暴露成更好用的函数,并加入异常处理。
5.1 头文件定义
// 文件路径:Source/YourProject/Public/JsonStructConverter.h #pragma once #include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" #include "JsonStructConverter.generated.h" UCLASS() class YOURPROJECT_API UJsonStructConverter : public UBlueprintFunctionLibrary { GENERATED_BODY() public: // 将任意结构体转成 JSON 字符串,蓝图可调用 UFUNCTION(BlueprintCallable, CustomThunk, Category = "Json|Struct", meta = (CustomStructureParam = "StructValue")) static FString StructToJsonString(const int32& StructValue); // 将 JSON 字符串转成任意结构体,蓝图可调用 UFUNCTION(BlueprintCallable, CustomThunk, Category = "Json|Struct", meta = (CustomStructureParam = "OutStructValue")) static bool JsonStringToStruct(const FString& JsonString, int32& OutStructValue); };这里的CustomThunk与CustomStructureParam是 UE5 蓝图节点支持任意结构体参数的关键。如果你希望测试 C++ 版本,可以看下面的具体实现。
5.2 CPP 实现
// 文件路径:Source/YourProject/Private/JsonStructConverter.cpp #include "JsonStructConverter.h" #include "JsonObjectConverter.h" FString UJsonStructConverter::StructToJsonString(const int32& StructValue) { // 这里不能直接使用 StructValue,必须用泛型方式。 // 在 CustomThunk 场景中,真正的实现可以通过 UScriptStruct 拿到结构体指针。 // 为便于理解,先用一个具体类型示例。 FPlayerInfo Info; Info.UserId = TEXT("10001"); Info.Nickname = TEXT("UE5玩家"); Info.Level = 30; Info.VipLevel = 3; FString OutJsonString; if (FJsonObjectConverter::UStructToJsonObjectString(FPlayerInfo::StaticStruct(), &Info, OutJsonString, 0, 0)) { return OutJsonString; } return TEXT("{}"); } bool UJsonStructConverter::JsonStringToStruct(const FString& JsonString, int32& OutStructValue) { FPlayerInfo OutInfo; if (FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &OutInfo, 0, 0)) { // 在这里把 OutInfo 拷贝到 OutStructValue。 // CustomThunk 的真实实现需要动态分配内存并复制字段。 return true; } return false; }上面的代码演示的是思路,并不能直接用于任意结构体。真正的通用实现需要用UKismetSystemLibrary::GenericStructureToJsonString风格的处理,或者自己解析泛型参数。
不过,作为工程验证,UE5 还提供了一种更直接的方式:使用 FJsonObjectConverter 的泛型模板函数。
5.3 使用模板函数在 C++ 中直接转换
如果你在 C++ 代码中已经知道结构体类型,使用模板函数是最干净的方式:
#include "JsonObjectConverter.h" FPlayerInfo Info; Info.UserId = TEXT("10001"); Info.Nickname = TEXT("UE5玩家"); Info.Level = 30; Info.VipLevel = 3; // Struct 转 JSON 字符串 FString JsonString; FJsonObjectConverter::UStructToJsonObjectString(Info, JsonString); // 打印结果 UE_LOG(LogTemp, Log, TEXT("Converted JSON: %s"), *JsonString); // JSON 字符串转回 Struct FPlayerInfo RestoredInfo; FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &RestoredInfo); // 验证字段 check(RestoredInfo.Level == 30);这种写法最直接,适合 C++ 项目内部使用。它的优点是编译器帮你做类型检查,而且嵌套结构体、数组都会被反射系统自动处理。
5.4 蓝图专用扩展
如果项目以蓝图为主,但有几个 C++ 工具函数,可以通过UPARAM(ref)、CustomStructureParam做一层桥接,或直接使用市面上成熟插件的蓝图节点。这里不展开插件推荐,重点强调思路:
- 暴露一个
StructToJsonString函数,输入任意结构体,输出 JSON 字符串。 - 暴露一个
JsonStringToStruct函数,输入 JSON 字符串和结构体引用,输出布尔值表示成功与否。
蓝图中的调用流程一般如下:
- 创建一个结构体变量。
- 给字段赋值。
- 调用
StructToJsonString。 - 输出值即为 JSON。
- 将 JSON 字符串传给节点
JsonStringToStruct。 - 目标结构体变量被填充。
6. 完整示例:从结构体到 JSON 再到结构体
下面用一个可运行的、更完整的小例子,展示双向转换的效果。
6.1 准备一个复杂结构体
为了验证数组和嵌套,我们继续使用前面的 FPlayerTeamInfo。在其中多包含一些常见字段类型,比如布尔、浮点、枚举。
UENUM(BlueprintType) enum class ETeamTier : uint8 { Bronze, Silver, Gold, Legend }; USTRUCT(BlueprintType) struct FTeamDetailInfo { GENERATED_BODY() UPROPERTY(BlueprintReadOnly) FString ServerName; UPROPERTY(BlueprintReadOnly) int32 MaxMemberCount = 10; UPROPERTY(BlueprintReadOnly) bool bIsPublic = true; }; USTRUCT(BlueprintType) struct FPlayerTeamInfo { GENERATED_BODY() UPROPERTY(BlueprintReadOnly) FString TeamId; UPROPERTY(BlueprintReadOnly) FString TeamName; UPROPERTY(BlueprintReadOnly) ETeamTier Tier = ETeamTier::Bronze; UPROPERTY(BlueprintReadOnly) FTeamDetailInfo Detail; UPROPERTY(BlueprintReadOnly) TArray<FPlayerInfo> Members; };6.2 C++ 完整转换函数
// 文件路径:Source/YourProject/Private/JsonStructConverter.cpp(片段) #include "JsonObjectConverter.h" FString ConvertTeamInfoToJson(const FPlayerTeamInfo& TeamInfo) { FString OutString; if (FJsonObjectConverter::UStructToJsonObjectString(TeamInfo, OutString)) { return OutString; } return FString(TEXT("{}")); } bool ConvertJsonToTeamInfo(const FString& JsonString, FPlayerTeamInfo& OutTeamInfo) { return FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &OutTeamInfo); }6.3 调用示例
void UYourActor::TestJsonConvert() { FPlayerTeamInfo TeamInfo; TeamInfo.TeamId = TEXT("T001"); TeamInfo.TeamName = TEXT("先锋小队"); TeamInfo.Tier = ETeamTier::Gold; FTeamDetailInfo Detail; Detail.ServerName = TEXT("Asia-1"); Detail.MaxMemberCount = 8; Detail.bIsPublic = false; TeamInfo.Detail = Detail; FPlayerInfo Player1; Player1.UserId = TEXT("10001"); Player1.Nickname = TEXT("UE5玩家"); Player1.Level = 30; Player1.VipLevel = 3; TeamInfo.Members.Add(Player1); FPlayerInfo Player2; Player2.UserId = TEXT("10002"); Player2.Nickname = TEXT("蓝图玩家"); Player2.Level = 12; Player2.VipLevel = 0; TeamInfo.Members.Add(Player2); FString JsonString; if (FJsonObjectConverter::UStructToJsonObjectString(TeamInfo, JsonString)) { UE_LOG(LogTemp, Log, TEXT("Converted JSON:\n%s"), *JsonString); } FPlayerTeamInfo RestoredTeam; if (FJsonObjectConverter::JsonObjectStringToUStruct(JsonString, &RestoredTeam)) { UE_LOG(LogTemp, Log, TEXT("Restored TeamName = %s, MemberCount = %d"), *RestoredTeam.TeamName, RestoredTeam.Members.Num()); if (RestoredTeam.Members.Num() > 0) { UE_LOG(LogTemp, Log, TEXT("First Member = %s, Level = %d"), *RestoredTeam.Members[0].Nickname, RestoredTeam.Members[0].Level); } } }这段代码就是整个双向转换的完整演示。它验证了嵌套结构体、枚举、布尔、数组都能被正确序列化和反序列化。
6.4 运行与验证结果
运行项目,在日志窗口中可以看到类似输出:
LogTemp: Converted JSON: { "TeamId": "T001", "TeamName": "先锋小队", "Tier": 3, "Detail": { "ServerName": "Asia-1", "MaxMemberCount": 8, "bIsPublic": false }, "Members": [ { "UserId": "10001", "Nickname": "UE5玩家", "Level": 30, "VipLevel": 3 }, { "UserId": "10002", "Nickname": "蓝图玩家", "Level": 12, "VipLevel": 0 } ] } LogTemp: Restored TeamName = 先锋小队, MemberCount = 2 LogTemp: First Member = UE5玩家, Level = 30判断成功的标准很简单:
- JSON 字符串中字段名与结构体属性名一致。
- 枚举类型输出为数字,这是 UE5 默认行为,可以根据需要自定义。
- 数组长度正确。
- 反序列化后的对象字段与原始对象一致。
如果失败,第一步检查 JSON 字符串格式是否为合法 JSON,第二步检查结构体属性名与 JSON 键名的匹配情况,第三步查看日志中是否有反射元数据生成失败的相关提示。
7. Struct 与 JSON 转换的常见问题与排查
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| JSON 转 Struct 后字段全默认值 | 属性没有 UPROPERTY 宏,反射系统未生成元数据 | 检查结构体定义 | 给每个需要转换的属性添加 UPROPERTY |
| 枚举转出来是数字而不是字符串 | UE5 默认枚举序列化为整数值 | 查看枚举定义和 FJsonObjectConverter 参数 | 自定义序列化逻辑,或使用 Name 方式将枚举转成字符串 |
| 数组为空 | 结构体数组没有 UPROPERTY,或者 JSON 键名不匹配 | 打印 JSON 字符串核对键名 | 补充 UPROPERTY,确保键名大小写与属性名一致 |
| 嵌套结构体转换失败 | 内层结构体没有 GENERATED_BODY 或没有 UPROPERTY | 检查内层结构体定义 | 为内层结构体补充 USTRUCT 和 UPROPERTY |
| 带中文内容的 JSON 转出来乱码 | 字符串编码与读写文件编码不一致 | 检查文件编码和日志显示编码 | 统一使用 UTF-8,日志窗口可设置字体 |
| 蓝图调用时报错结构体类型不匹配 | 节点没有使用 CustomStructureParam | 检查蓝图节点实现 | 使用 CustomThunk 和 CustomStructureParam 暴露通用节点 |
| Json解析失败但没有明确日志 | JSON 字符串不合法,或字段类型无法强转 | 用在线 JSON 校验工具检查格式 | 对 JsonString 做卫生检查,并在转换失败时打印原始字符串 |
| FJsonObjectConverter 在不同 UE 小版本中表现差异 | 引擎接口参数有变化 | 查看当前引擎版本源码 | 以当前项目中头文件为准,不要盲从旧教程 |
8. 最佳实践与工程建议
8.1 结构体属性必须全部加上 UPROPERTY
没有 UPROPERTY 的属性不会进入反射系统,自然也不会被转换。这是初次接触这个功能的人最容易犯的错。
8.2 统一字段命名规范
Struct 属性名通常使用 PascalCase,JSON 键名一般使用 camelCase。FJsonObjectConverter 默认直接使用属性名作为键名,但你可以自定义命名策略。建议是:内部通信保持 CamelCase 统一,对外接口用转换层重新映射字段名,不要散落在业务代码里。
8.3 不要对超大数组做频繁全量转换
一次转换一万个结构体的性能开销是可观的。如果存档或网络包特别大,建议分批、流式或压缩传输。不要等到线上卡顿才回头改。
8.4 做好失败回退
JSON 转 Struct 时,一旦服务器返回结构变化,可能导致字段缺失或类型不匹配。应该为关键结构体提供默认值,并在转换失败时走兜底逻辑,而不是直接崩溃。
8.5 敏感数据不能直接序列化到 JSON 日志
账号 token、支付凭证、用户隐私字段,在打印 JSON 前要做过滤。绝不把完整 JSON 直接打到日志里。
8.6 版本兼容与升级
当结构体字段增减时,旧存档或旧服务器数据可能无法转换。建议在 JSON 中保留一个 version 字段,读取时做版本判断和迁移。这样能避免一次结构体改动引发所有旧数据不可用。
8.7 单元测试
在 C++ 层为核心结构体写自动化的序列化反序列化测试,尤其是网络和存档相关的结构体。每次修改结构体定义后跑一遍测试,能省下大量联调时间。
8.8 插件与源码的取舍
如果你使用第三方 StructJsonString 插件,先检查源码是否开放、是否适配当前 UE 版本、是否支持蓝图调用、是否有额外的第三方依赖。如果只是 C++ 项目,内置 FJsonObjectConverter 基本够用;如果蓝图项目需要通用节点,成熟插件会更省事。
9. 总结与后续学习方向
Struct 与 JSON 的双向转换在 UE5 开发里不是一个炫技功能,而是一个基础工程能力。它直接影响网络层、存档层、配置层的开发效率与维护成本。
从底层看,UE5 的反射系统让这种通用转换成为可能;从实现看,FJsonObjectConverter 已经把核心工作做完;从工程角度看,值得投入的并不是重复造轮子,而是封装出适合自己项目的调用方式、错误处理、命名策略和测试用例。
这篇文章主要解决了三个问题:
- 让你明白 Struct 和 JSON 能自动互转,靠的是反射系统,而不是魔法。
- 给出了 C++ 环境下可复制的完整转换示例。
- 列出了实际项目里最常见的坑和最佳实践。
后续你可以继续学习:
- FJsonObjectConverter 的完整源码与参数含义。
- CustomThunk 蓝图节点的实现细节。
- UE5 中 Enum 和 DateTime 等特殊类型的自定义序列化方式。
- JSON Schema 或字段版本迁移在 UE5 中的落地方法。
建议收藏备用。下次再遇到“手写 JSON 解析”的冲动时,先停下来想清楚:是直接加一个 UPROPERTY 就能解决的事,还是应该从架构层面重新设计数据结构。