You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

继承类的JSON序列化与反序列化问题求助

继承类的JSON序列化与反序列化问题求助

看起来你在处理基于接口的JSON序列化/反序列化时遇到了典型的类型信息丢失问题——Newtonsoft.Json默认不知道该把IDamageType接口反序列化成哪个具体实现类(比如你的空Fire类),序列化空实现类时只会输出空对象{},反序列化时更没法自动映射到Fire实例。结合你的Unity游戏场景,我来给你梳理几个实用的解决方案:


问题根源拆解

当你序列化WeaponStats中的IDamageType属性时:

  1. Json.NET无法识别接口对应的具体实现类(比如Fire),所以序列化后仅输出空对象{}
  2. 反序列化时,看到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"
}

注意:替换YourNamespaceFire类实际所在的命名空间,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被篡改时可能注入恶意类型)快速原型开发,或内部工具生成配置
自定义ConverterJSON结构简洁,设计师易维护;扩展性强(加新类型只需修改Converter和JSON)需要额外开发Converter正式游戏项目,需要设计师维护配置的场景

推荐方案二,因为游戏配置文件通常需要非技术人员参与维护,简洁直观的JSON结构能降低沟通成本,而且自定义Converter的扩展性更好,未来给Fire加属性或新增伤害类型(比如Ice)时,改动量极小。


针对空类Fire的扩展提示

现在Fire是空类,以后如果要添加属性(比如BurnDurationBurnDamage):

  • 方案一:无需修改代码,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实例,完美兼容现有代码。

火山引擎 最新活动