从磁盘加载文件并在 json 反序列化期间将其用作值

问题描述 投票:0回答:1

为了明确起见,我将使用 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"
  },
}
c# json json.net json-deserialization godot
1个回答
0
投票

如果在反序列化过程中您需要做的就是从给定路径的磁盘加载

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 以外的值进行反序列化时,应使用自定义

    SerializationBinder 验证传入类型。

    有关详细信息,请参阅 Newtonsoft Json 中的

    TypeNameHandling 注意事项由于 Json.Net TypeNameHandling auto 而导致外部 json 易受攻击?

模型小提琴

在这里

© www.soinside.com 2019 - 2024. All rights reserved.