如何为Newtonsoft Json.NET自定义序列化检查,仅保存预制体变更值?
实现预制体对象仅序列化变更值的Json.NET方案
嘿,这个需求我刚好在开发游戏场景编辑器的时候踩过坑,针对预制体只保存和原始模板不一样的属性值,Json.NET有几种非常灵活的自定义判断方案,比单纯用DefaultValue特性要靠谱得多,给你分享三个实用的思路:
方案1:用ShouldSerialize[PropertyName]方法(最快上手)
Json.NET有个默认的约定:如果你的类里有命名为ShouldSerialize[属性名]的布尔方法,序列化时会自动调用这个方法来决定是否写入该属性。
举个实际的例子,假设你的预制体对象有Position和Scale两个属性,我们可以给每个属性加对应的判断方法:
public class PrefabInstance { // 保存从预制体模板加载的原始值 private Vector3 _originalPosition; private Vector3 _originalScale; public Vector3 Position { get; set; } public Vector3 Scale { get; set; } // 加载预制体时初始化原始值 public void InitFromPrefab(PrefabTemplate template) { _originalPosition = template.Position; _originalScale = template.Scale; Position = template.Position; Scale = template.Scale; } // 自定义判断:只有Position和原始值不一样才序列化 public bool ShouldSerializePosition() { return !Position.Equals(_originalPosition); } // 同理处理Scale public bool ShouldSerializeScale() { return !Scale.Equals(_originalScale); } }
这样序列化的时候,Json.NET会自动跳过和原始预制体值一致的属性,只写入变更过的内容,完全不用额外配置序列化器。
方案2:自定义IContractResolver(全局统一处理)
如果你的预制体类型很多,不想每个类都写一堆ShouldSerialize方法,可以搞个全局的契约解析器,统一处理所有预制体对象的序列化逻辑。
首先我们可以定义一个标记接口,用来识别哪些是预制体实例:
public interface IPrefabTracked { // 提供一个方法,根据属性名获取原始预制体的值 object GetOriginalValue(string propertyName); }
然后让你的预制体类实现这个接口,用字典存原始值会比较灵活:
public class PrefabInstance : IPrefabTracked { private readonly Dictionary<string, object> _originalValues = new(); public Vector3 Position { get; set; } public Vector3 Scale { get; set; } public void InitFromPrefab(PrefabTemplate template) { _originalValues[nameof(Position)] = template.Position; _originalValues[nameof(Scale)] = template.Scale; Position = template.Position; Scale = template.Scale; } // 实现接口方法,返回对应属性的原始值 public object GetOriginalValue(string propertyName) { _originalValues.TryGetValue(propertyName, out var value); return value; } }
接下来写自定义的契约解析器,替换默认的序列化判断逻辑:
public class PrefabContractResolver : DefaultContractResolver { protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { var property = base.CreateProperty(member, memberSerialization); // 只处理实现了IPrefabTracked接口的类型 if (property.DeclaringType.GetInterface(nameof(IPrefabTracked)) != null) { property.ShouldSerialize = instance => { var prefabInstance = (IPrefabTracked)instance; var currentValue = member.GetValue(instance); var originalValue = prefabInstance.GetOriginalValue(member.Name); // 对比当前值和原始值,不一样才序列化 return !object.Equals(currentValue, originalValue); }; } return property; } }
最后序列化的时候指定这个解析器就行:
var serializerSettings = new JsonSerializerSettings { ContractResolver = new PrefabContractResolver(), Formatting = Formatting.Indented // 可选,格式化JSON方便阅读 }; var sceneJson = JsonConvert.SerializeObject(yourSceneObjects, serializerSettings);
这个方案的好处是一劳永逸,所有预制体类型都能自动套用这个逻辑,不用重复写代码。
方案3:自定义特性+契约解析器(精确控制属性)
如果你只想给部分属性做变更跟踪,不想全局处理,可以自定义一个特性,标记需要检查的属性,再结合契约解析器来处理。
先定义特性:
[AttributeUsage(AttributeTargets.Property)] public class TrackPrefabChangeAttribute : Attribute { }
然后在预制体类的属性上标记:
public class PrefabInstance { private Vector3 _originalPosition; private Vector3 _originalScale; [TrackPrefabChange] public Vector3 Position { get; set; } [TrackPrefabChange] public Vector3 Scale { get; set; } // 初始化原始值的逻辑... public void InitFromPrefab(PrefabTemplate template) { _originalPosition = template.Position; _originalScale = template.Scale; Position = template.Position; Scale = template.Scale; } }
然后修改契约解析器,只处理带有这个特性的属性:
public class TrackedPrefabContractResolver : DefaultContractResolver { protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { var property = base.CreateProperty(member, memberSerialization); // 检查属性是否标记了TrackPrefabChangeAttribute var trackAttr = member.GetCustomAttribute<TrackPrefabChangeAttribute>(); if (trackAttr != null && member is PropertyInfo propInfo) { property.ShouldSerialize = instance => { var prefabObj = (PrefabInstance)instance; object originalValue = null; // 根据属性名获取原始值,这里也可以用反射或者字典优化 switch (propInfo.Name) { case nameof(PrefabInstance.Position): originalValue = prefabObj._originalPosition; break; case nameof(PrefabInstance.Scale): originalValue = prefabObj._originalScale; break; } var currentValue = propInfo.GetValue(instance); return !object.Equals(currentValue, originalValue); }; } return property; } }
这个方案适合你需要精确控制哪些属性需要跟踪变更的场景,灵活性很高。
一些注意点
- 对于自定义复杂类型,一定要正确实现
Equals方法,否则会导致变更判断不准确。 - 如果原始值是引用类型,要注意做深拷贝,避免原始值被修改后判断逻辑失效。
- 可以配合
JsonSerializerSettings里的NullValueHandling = NullValueHandling.Ignore,但要确保和你的变更判断逻辑不冲突。
内容的提问来源于stack exchange,提问作者jsmars
相关产品推荐
相关产品推荐

