为了明确起见,我将使用 Newtonsoft.Json 13.0.3 提出该问题的可行解决方案。上述解决方案看起来不太好,我正在寻找更好的解决方案。
一些上下文,这是 Godot,因此磁盘上的文件属于“Resource”类型,并且通过调用 ResourceLoader.Load(string path) 来加载它们
这个想法是序列化一个将加载的资源作为参数的类型,并通过加载资源而不是创建一个新资源来反序列化它。
public class KnownTypesBinder : ISerializationBinder
{
public string CurrentResourcePath { get; private set; }
public Type BindToType(string assemblyName, string typeName)
{
Type prospectType = Type.GetType(typeName);
if (prospectType == null)
{
// When the typeName contains an '@' it means it is a resource type with its path encoded after the '@' character
int strIndex = typeName.Find('@');
Assert.IsTrue(strIndex != -1);
// Storing the path of the currently deserialized resource for future use by the contract
CurrentResourcePath = typeName.Substring(strIndex + 1);
string realTypeName = typeName.Substr(0, strIndex);
return Type.GetType(realTypeName);
}
else
{
return Type.GetType(typeName);
}
}
public void BindToName(Type serializedType, out string assemblyName, out string typeName)
{
if (typeof(Resource).IsAssignableFrom(serializedType))
{
// What is done here is that for resource types we want to bake the ResourcePath after the resource type info
// like this: "$type": "Godot.Resource@res://MyPath/MyResource.tres"
// The "Godot.Resource" part is the type info
// The "res://MyPath/MyResource.tres" is the godot path of the file
// BindToName does not provide with the currently serialized object, so it is grabed from the contract resolver who is the one knowing that.
// It is then cast to Resource type
assemblyName = null;
Resource currentlySerializedResource = (CoreLogic.Instance.JsonSerializingSettings.ContractResolver as ResourceContractResolver).CurrentlySerializedObject as Resource;
Assert.IsTrue(currentlySerializedResource != null);
string path = currentlySerializedResource.ResourcePath;
typeName = serializedType.FullName + "@" + path;
}
else
{
string id = serializedType.FullName;
assemblyName = null; // That should probably be filled, but is outside the scope of this discussion
typeName = id;
}
}
}
class ResourceContractResolver : DefaultContractResolver
{
// Points to the object that is about to be serialized, as soon as it will start serializing properties
// within that object CurrentlySerializedObject will change to that property's value.
public object CurrentlySerializedObject { get; private set; }
protected override JsonObjectContract CreateObjectContract(Type objectType)
{
JsonObjectContract contract = base.CreateObjectContract(objectType);
if (typeof(Resource).IsAssignableFrom(objectType))
{
// Instance is not created right away:
contract.DefaultCreator = () =>
{
string currentResourcePath = (CoreLogic.Instance.JsonSerializingSettings.SerializationBinder as KnownTypesBinder).CurrentResourcePath;
return ResourceLoader.Load(currentResourcePath);
};
}
return contract;
}
protected override List<MemberInfo> GetSerializableMembers(Type objectType)
{
if (typeof(Resource).IsAssignableFrom(objectType))
{
// Do not serialized anything for resources
return new List<MemberInfo>();
}
else
{
return base.GetSerializableMembers(objectType);
}
}
protected override JsonContract CreateContract(Type objectType)
{
JsonContract contract = base.CreateContract(objectType);
contract.OnSerializingCallbacks.Add(OnSerializingCallback);
return contract;
}
private void OnSerializingCallback(object o, StreamingContext context)
{
CurrentlySerializedObject = o;
}
}
为了使该示例完整,我必须将这两个类挂钩到 JsonSerializationSettings 等,但我希望您明白这一点。
该解决方案的明显问题在于,它本质上是一种基于某些事情按特定顺序发生的知识的黑客攻击。如果我升级 newtonsoft.json 版本,很容易失败,并使我的代码更难维护。
如果有更优雅的解决方案,我将不胜感激。
Edit1:为了清晰起见,编辑了类型+路径串联的代码注释 Edit2:底部评论也指出了该解决方案潜在的线程安全问题。
Edit3:添加序列化的 json 在此示例中的样子:
public class ContainerType
{
public int ValueInt = -1;
public float ValueFloat = 3.14f;
public Resource Resource = null; // set to loaded resource file at path res://MyPath/MyResource.tres before serialization
}
{
"$type": "ContainerType",
"ValueInt": -1,
"ValueFloat": 3.14,
"Resource": {
"$type": "Godot.Resource@res://MyPath/MyResource.tres"
},
}
Resource
,则创建一个 自定义 JsonConverter<Resource>
来执行此操作,写入和读取 ResourcePath
会容易得多
转换为 JSON,而不是资源本身。
首先定义以下转换器:
public class ResourceConverter : JsonConverter<Resource>
{
public override void WriteJson(JsonWriter writer, Resource? value, JsonSerializer serializer) =>
writer.WriteValue(value?.ResourcePath);
public override Resource? ReadJson(JsonReader reader, Type objectType, Resource? existingValue, bool hasExistingValue, JsonSerializer serializer) =>
reader.MoveToContentAndAssert().TokenType switch
{
JsonToken.Null => null,
JsonToken.String => reader.Value is string s ? ResourceLoader.Load(s) : throw new JsonSerializationException("Invalid reader.Value for resource"),
_ => throw new JsonSerializationException(),
};
}
public static partial class JsonExtensions
{
public static JsonReader MoveToContentAndAssert(this JsonReader reader)
{
ArgumentNullException.ThrowIfNull(reader);
if (reader.TokenType == JsonToken.None) // Skip past beginning of stream.
reader.ReadAndAssert();
while (reader.TokenType == JsonToken.Comment) // Skip past comments.
reader.ReadAndAssert();
return reader;
}
public static JsonReader ReadAndAssert(this JsonReader reader)
{
ArgumentNullException.ThrowIfNull(reader);
if (!reader.Read())
throw new JsonReaderException("Unexpected end of JSON stream.");
return reader;
}
}
然后消除
ResourceContractResolver
并从Resource
中删除所有与KnownTypesBinder
相关的逻辑:
public class KnownTypesBinder : ISerializationBinder
{
public Type BindToType(string? assemblyName, string typeName)
{
// TODO: replace with a binder that actually checks known types
// For why, see https://stackoverflow.com/questions/39565954/typenamehandling-caution-in-newtonsoft-json
return Type.GetType(typeName)!;
}
public void BindToName(Type serializedType, out string? assemblyName, out string? typeName)
{
var id = serializedType.FullName;
assemblyName = null; // That should probably be filled, but is outside the scope of this discussion
typeName = id;
}
}
然后序列化和反序列化你的
Container
,如下所示:
var container = new ContainerType
{
Resource = ResourceLoader.Load("res://MyPath/MyResource.tres"),
};
var settings = new JsonSerializerSettings
{
Converters = { new ResourceConverter() },
// Add other settings as required, e.g.:
TypeNameHandling = TypeNameHandling.Objects, // Be sure this is really necessary.
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
SerializationBinder = new KnownTypesBinder(),
};
var json = JsonConvert.SerializeObject(container, Formatting.Indented, settings);
var container2 = JsonConvert.DeserializeObject<ContainerType>(json, settings);
资源的值将作为其
ResourcePath
字符串进行往返,并在反序列化期间使用 ResourceLoader.Load()
加载:
{
"$type": "ContainerType",
"ValueInt": -1,
"ValueFloat": 3.14,
"Resource": "res://MyPath/MyResource.tres"
}
备注:
您当前的代码将资源类型添加到资源路径之前。 反序列化期间您似乎并不真正需要它,但如果需要,您可以修改
ReadJson()
和 WriteJson()
以包含它。
请注意,
TypeNameHandling
存在安全风险。正如 docs 中所解释的:
当您的应用程序从外部源反序列化 JSON 时,应谨慎使用 TypeNameHandling。使用 None 以外的值进行反序列化时,应使用自定义有关详细信息,请参阅 Newtonsoft Json 中的SerializationBinder 验证传入类型。
TypeNameHandling 注意事项 和 由于 Json.Net TypeNameHandling auto 而导致外部 json 易受攻击?。
在这里。