继承类的JSON序列化与反序列化问题求助
看起来你在处理基于接口的JSON序列化/反序列化时遇到了典型的类型信息丢失问题——Newtonsoft.Json默认不知道该把IDamageType接口反序列化成哪个具体实现类(比如你的空Fire类),序列化空实现类时只会输出空对象{},反序列化时更没法自动映射到Fire实例。结合你的Unity游戏场景,我来给你梳理几个实用的解决方案:
问题根源拆解
当你序列化WeaponStats中的IDamageType属性时:
- Json.NET无法识别接口对应的具体实现类(比如
Fire),所以序列化后仅输出空对象{}; - 反序列化时,看到
IDamageType接口类型,默认不知道要实例化哪个子类,导致要么得到非预期的匿名对象,要么直接报错。
你的Fire类目前是空的,但以后大概率会扩展属性(比如灼烧时长、持续伤害),所以解决方案需要兼顾当前空类场景和未来扩展需求。
方案一:使用TypeNameHandling自动保留类型元数据(快速适配)
这个方法会让Json.NET在序列化时把具体类型的元数据写入JSON,反序列化时根据元数据实例化正确的类,适合快速改造现有代码。
1. 修改反序列化配置
在你的JSONCalculator<T>的反序列化代码中,添加TypeNameHandling.Auto配置:
public static T GetEquipmentStats(string fileName,int level) { string newPath = Path.Combine(Environment.CurrentDirectory, (path + fileName+".json")); string jsonString = File.ReadAllText(newPath); // 添加JsonSerializerSettings,启用类型元数据处理 var serializerSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto, // Unity环境下关闭空字段输出,避免干扰 NullValueHandling = NullValueHandling.Ignore }; Dictionary<string, T> results = JsonConvert.DeserializeObject<Dictionary<string,T>>(jsonString, serializerSettings); // 优化等级映射,替代冗余的switch var levelMap = new Dictionary<int, string> { {0, "levelZero"}, {1, "levelOne"}, {2, "levelTwo"}, {3, "levelThree"} }; if (!levelMap.TryGetValue(level, out string levelStr)) { throw new ArgumentOutOfRangeException(nameof(level), $"不支持的装备等级:{level}"); } return results[levelStr]; }
2. 生成包含类型信息的JSON
如果要序列化WeaponStats(比如生成新的装备配置),同样需要用这个配置,这样JSON会包含类型元数据:
var sampleStaff = new WeaponStats { Damage = 10, DamageType = new Fire(), Range = 4, // 其他属性... }; string json = JsonConvert.SerializeObject(sampleStaff, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto, Formatting = Formatting.Indented // 格式化JSON,方便阅读 });
生成的JSON中DamageType会变成:
"DamageType": { "$type": "YourNamespace.Fire, Assembly-CSharp" }
注意:替换
YourNamespace为Fire类实际所在的命名空间,Unity中默认程序集是Assembly-CSharp,如果用了自定义程序集要对应修改。
3. 适配现有Staff.json
手动修改现有Staff.json中的"DamageType": {}为上面带$type的结构,反序列化时就能正确得到Fire实例。
方案二:自定义JsonConverter(推荐,适合游戏配置场景)
如果不想在JSON中加入$type这类技术元数据(避免让游戏设计师困惑),可以自定义转换器,通过标识字段来指定具体的伤害类型,JSON结构更简洁易维护。
1. 修改JSON配置格式
把Staff.json中的"DamageType": {}改成更直观的标识形式,比如字符串:
"levelZero": { "Damage": 10, "DamageType": "Fire", // 用字符串标识具体类型 "Range": 4, // 其他属性... }
如果以后Fire要加属性(比如灼烧时长),可以改成对象形式:
"DamageType": { "Type": "Fire", "BurnDuration": 2, "BurnDamage": 3 }
2. 实现自定义JsonConverter
public class DamageTypeConverter : JsonConverter<IDamageType> { // 反序列化:从JSON读取并实例化正确的伤害类型 public override IDamageType ReadJson(JsonReader reader, Type objectType, IDamageType existingValue, bool hasExistingValue, JsonSerializer serializer) { // 兼容两种JSON格式:字符串标识 或 带Type字段的对象 if (reader.TokenType == JsonToken.String) { string typeName = (string)reader.Value; return CreateDamageType(typeName); } else if (reader.TokenType == JsonToken.StartObject) { JObject obj = JObject.Load(reader); string typeName = obj["Type"].Value<string>(); var damageType = CreateDamageType(typeName); // 自动填充属性(适配未来Fire类扩展) serializer.Populate(obj.CreateReader(), damageType); return damageType; } throw new JsonSerializationException($"无法解析伤害类型:{reader.TokenType}"); } // 序列化:把具体伤害类型转为JSON格式 public override void WriteJson(JsonWriter writer, IDamageType value, JsonSerializer serializer) { Type type = value.GetType(); // 空类直接输出类型名称字符串 if (type.GetProperties().Length == 0) { writer.WriteValue(type.Name); } else { // 有属性时输出带Type字段的对象 JObject obj = JObject.FromObject(value); obj.AddFirst(new JProperty("Type", type.Name)); obj.WriteTo(writer); } } // 统一创建伤害类型实例的逻辑(未来加新类型只需加case) private IDamageType CreateDamageType(string typeName) { return typeName switch { "Fire" => new Fire(), // 以后加Ice、Thunder等类型,直接加对应的case _ => throw new NotSupportedException($"不支持的伤害类型:{typeName}") }; } }
3. 在JSONCalculator中启用自定义转换器
public static T GetEquipmentStats(string fileName,int level) { string newPath = Path.Combine(Environment.CurrentDirectory, (path + fileName+".json")); string jsonString = File.ReadAllText(newPath); var serializerSettings = new JsonSerializerSettings { Converters = new List<JsonConverter> { new DamageTypeConverter() }, NullValueHandling = NullValueHandling.Ignore }; Dictionary<string, T> results = JsonConvert.DeserializeObject<Dictionary<string,T>>(jsonString, serializerSettings); // 优化等级映射 var levelMap = new Dictionary<int, string> { {0, "levelZero"}, {1, "levelOne"}, {2, "levelTwo"}, {3, "levelThree"} }; if (!levelMap.TryGetValue(level, out string levelStr)) { throw new ArgumentOutOfRangeException(nameof(level), $"不支持的装备等级:{level}"); } return results[levelStr]; }
方案对比与推荐
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| TypeNameHandling | 代码修改少,快速生效 | JSON包含技术元数据,设计师不易理解;存在一定安全风险(JSON被篡改时可能注入恶意类型) | 快速原型开发,或内部工具生成配置 |
| 自定义Converter | JSON结构简洁,设计师易维护;扩展性强(加新类型只需修改Converter和JSON) | 需要额外开发Converter | 正式游戏项目,需要设计师维护配置的场景 |
推荐方案二,因为游戏配置文件通常需要非技术人员参与维护,简洁直观的JSON结构能降低沟通成本,而且自定义Converter的扩展性更好,未来给Fire加属性或新增伤害类型(比如Ice)时,改动量极小。
针对空类Fire的扩展提示
现在Fire是空类,以后如果要添加属性(比如BurnDuration、BurnDamage):
- 方案一:无需修改代码,TypeNameHandling会自动序列化/反序列化新属性;
- 方案二:只需在
Fire类中加属性,JSON中对应修改DamageType的结构,Converter的ReadJson方法会通过serializer.Populate自动填充属性,无需额外改动逻辑。
比如扩展后的Fire类:
public class Fire : IDamageType { public int BurnDuration { get; set; } public int BurnDamage { get; set; } }
对应的JSON:
"DamageType": { "Type": "Fire", "BurnDuration": 2, "BurnDamage": 3 }
反序列化后就能得到带属性的Fire实例,完美兼容现有代码。




