【问题标题】:Newtonsoft Calls Getter on Deserializing a PropertyNewtonsoft 调用 Getter 反序列化属性
【发布时间】:2023-09-04 05:29:01
【问题描述】:

我创建了一个简单的类模型 (AnchorMetaData),如下所示,其中有两个项目。一个是列表字段 (Vector3),由于它的制作方式而无法序列化,因此我为类 (SerializableVector3) 创建了一个可以序列化和反序列化的属性。我希望在 Newtonsoft 中使用这个属性来保存/加载模型。

该类保存得很好但是,当我尝试从 JSON 反序列化模型时,它调用 AttachedTaskLocations 属性的 getter 而不是 setter。这使得要初始化的字段为空。

我只是通过使用日志消息并设置一些断点才注意到这一点。反序列化时它从不调用设置器。这很奇怪,因为它应该起作用。

另一个奇怪的行为是它确实会在SerializableVector3 的 x、y、z 的设置器上暂停,并使用文件中的正确值。这太奇怪了。

我正在使用 Unity 2019.1.14,但没有它也应该可以工作,只需将矢量列表更改为您拥有的东西。

当我加载显示的 JSON 文件时,该文件是通过序列化 AnchorMetaData 创建的,它在 attachedTaskLocations 中有零项。为什么会这样?为什么 setter 没有被调用?


我创建的用于保存/加载Vector3 的类称为SerializableVector3。 我希望保存/加载的类:

[Serializable]
public class AnchorMetaData
{
    // Cannot serialize this.
    [JsonIgnore]
    public List<Vector3> attachedTaskLocations = new List<Vector3>();

    /// <summary>
    /// This property servers as an interface for JSON de-/serialization.
    /// It uses a class that can be serialized by Newtonsoft.
    /// Should not be used in code except for serialization purposes.
    /// </summary>
    [JsonProperty("AttachedTaskLocations")]
    public List<SerializableVector3> AttachedTaskLocations
    {
        get
        {
            Debug.Log("Writing serialized vector.");
            return attachedTaskLocations
                .Select(vector3 => new SerializableVector3(vector3))
                .ToList();
        }
        set
        {
            Debug.Log("Loading serialized vector.");
            attachedTaskLocations = value
                .Select(sVector3 => new Vector3(sVector3.x, sVector3.y, sVector3.z))
                .ToList();
        }
    }

}

序列化 JSON:

{
  "AttachedTaskLocations": [
      {
        "x": 1.0,
        "y": 1.0,
        "z": 1.0
      },
      {
        "x": 1E+12,
        "y": 2.0,
        "z": 3.0
      },
      {
        "x": 0.0,
        "y": 0.0,
        "z": 0.0
      }
    ]
  }

当断点命中反序列化的 getter 时堆栈。

【问题讨论】:

  • 是否有相关/有意只标记y [JsonProperty]xz 不是?
  • 我尝试添加 private List&lt;SerializableVector3&gt; _atl 并将 get 更改为 { Debug.Log(...); return _atl; }。我将_atl = value; 添加到您拥有的设置器中。现在调用get,然后设置。我尝试了一些变体,如果我在 getter 中除了 return _atl; 之外还有任何有意义的东西,那么永远不会调用 setter。我认为 Newtonsoft 在使用支持字段序列化属性方面有点太聪明了。
  • 我应该说,“它认为可以计算的属性太聪明了”。如果记录在案,也许有一个属性可以修改行为。否则,私有字段解决方案很愚蠢,但很实用,评论很丰富。
  • @EdPlunkett 感谢您抽出宝贵时间进行调查。这有点为我指明了正确的方向。因此,如果 getter 在调用时返回 null,它会调用 setter,否则不会。奇怪,我不知道为什么会这样,但确实如此。很高兴知道。如果您想写一个答案,我可以解决这个问题,直到出现更好的解决方案。
  • 顺便说一句,您可以为 Unity 对象(例如 Vector3)使用自定义合约解析器和自定义序列化器。要查找(并阅读相关文档)的类是 DefaultContractResolverJsonConverter。不幸的是,我在 GitHub 上没有示例,但它非常简单。

标签: c# unity3d serialization json.net deserialization


【解决方案1】:

反序列化后你的AttachedTaskLocations为空的原因有两个:

  1. 默认情况下,Json.Net 将在反序列化期间重用现有对象值,而不是创建新对象值。因此,对于诸如 AttachedTaskLocations 列表之类的属性,它会首先调用 getter,然后找到现有值,然后继续从 JSON 中填充它。
  2. AttachedTaskLocations 的 getter 不会每次都返回相同的实例;它总是从 attachedTaskLocations 支持字段创建一个新实例。

看来发生的事情是这样的:

  1. 序列化程序调用AttachedTaskLocations getter,它返回一个新的空列表。
  2. 序列化程序从 JSON 填充该列表。
  3. 填充的列表被丢弃(序列化程序假定AnchorMetaData 实例已经引用了该列表,因此它永远不会调用设置器)。
  4. 当您稍后访问 AttachedTaskLocations getter 时,它会再次返回一个新的空列表。

您可以通过将ObjectCreationHandling 设置为Replace 来更改序列化程序的行为。仅此更改似乎就可以解决问题in my testing

但是,我认为当有更好的解决方案时,您会在这里跳过一堆箍来让Vector3 正确序列化/反序列化:使用自定义JsonConverter。这是转换器所需的代码。没那么多:

public class Vector3Converter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(Vector3);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.StartObject)
        {
            JObject obj = JObject.Load(reader);
            return new Vector3((float)obj["x"], (float)obj["y"], (float)obj["z"]);
        }
        if (reader.TokenType == JsonToken.Null)
        {
            return null;
        }
        throw new JsonException("Unexpected token type: " + reader.TokenType);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        if (value != null)
        {
            Vector3 vector = (Vector3)value;
            JObject obj = new JObject(
                new JProperty("x", vector.x),
                new JProperty("y", vector.y),
                new JProperty("z", vector.z)
            );
            obj.WriteTo(writer);
        }
        else
        {
            JValue.CreateNull().WriteTo(writer);
        }
    }
}

有了这个转换器,您可以完全摆脱 SerializableVector3 类,您可以将您的 AnchorMetaData 类简化为:

public class AnchorMetaData
{
    [JsonProperty("AttachedTaskLocations")]
    public List<Vector3> AttachedTaskLocations { get; set; } = new List<Vector3>();
}

要使用转换器,您可以:

  • 将其传递给JsonConvert.SerializeObject()/DeserializeObject() 方法;
  • 将其添加到JsonSerializerSettings 上的Converters 集合并将设置传递给JsonConvert.SerializeObject()/DeserializeObject(),或者
  • 直接在JsonSerializer 实例上将其添加到Converters 集合中。

例如:

var settings = new JsonSerializerSettings();
settings.Converters.Add(new Vector3Converter());
var metaData = JsonConvert.DeserializeObject<AnchorMetaData>(json, settings);

往返演示:https://dotnetfiddle.net/jmYIq9


如果您无权访问序列化程序(很难从您的问题中判断您是在自己的代码中进行序列化/反序列化,还是某些第三方组件正在处理),那么另一种使用方式转换器是通过属性。对于像AttachedTaskLocations 这样的列表属性,您可以在[JsonProperty] 属性中指定ItemConverterType,如下所示:

    [JsonProperty("AttachedTaskLocations", ItemConverterType = typeof(Vector3Converter))]
    public List<Vector3> AttachedTaskLocations { get; set; } = new List<Vector3>();

如果您有单个实例属性,那么您将使用 [JsonConverter] 属性,如下所示:

    [JsonConverter(typeof(Vector3Converter))]
    public Vector3 SingleVector { get; set; }

小提琴:https://dotnetfiddle.net/yxwqDL

【讨论】:

  • it always creates a new instance from the attachedTaskLocations backing field 当然!就是这样^^
  • 感谢您的精彩解释。当它调用 getter 时我不丢弃返回的列表时,它确实有效,知道它是如何工作的。我确实尝试了替换选项,但没有帮助。它仍然只是调用 getter 而不是像你这样的 setter。这是我使用的代码,有什么明显的我做错了吗? serializerSettings.ObjectCreationHandling = ObjectCreationHandling.Replace; string jsonContent = TextFileReaderWriter.ReadTextFromFile(filePath); luSerializedObject = JsonConvert.DeserializeObject&lt;LuSerializedObject&gt;(jsonContent, serializerSettings);
  • 这对我来说是正确的。我不知道为什么ObjectCreationHandling.Replace 不适合你。您是否尝试过转换器方法?
  • 是的。转换器方法效果很好。感谢您花时间回答这个问题。
  • 没问题;很高兴我能帮上忙!