Newtonsoft Calls Getter при десериализации собственности - PullRequest
0 голосов
/ 06 ноября 2019

Я создал простую модель класса (AnchorMetaData), показанную ниже, в которой есть два элемента. Одним из них является поле списка (Vector3), которое нельзя сериализовать из-за его создания, поэтому я создал свойство для класса (SerializableVector3), которое можно сериализовать и десериализовать. Я надеялся использовать это свойство с Newtonsoft для сохранения / загрузки модели.

Класс сохраняет очень хорошо, однако, когда я пытаюсь десериализовать модель из JSON, он вызывает метод получения AttachedTaskLocations свойства, а не setter. Это делает поле для инициализации пустым.

Я заметил это только с помощью сообщения логов и создания некоторых точек останова. Он никогда не вызывает сеттер при десериализации. Что очень странно, так как должно функционировать.

Другое странное поведение заключается в том, что он останавливается на установщике x, y, z, равном SerializableVector3, с правильными значениями из файла. Это супер странно.

Я работаю с Unity 2019.1.14, но это должно работать и без него, просто измените список векторов на что-то, что у вас есть.

Когда я загружаю показанный файл JSON, который был создан сериализацией AnchorMetaData, он имеет ноль элементов в attachedTaskLocations. Почему это происходит? Почему не вызывается сеттер?


Класс I, созданный для сохранения / загрузки 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 call stack

1 Ответ

3 голосов
/ 07 ноября 2019

Причина того, что ваша AttachedTaskLocations пуста после десериализации, двояка:

  1. По умолчанию Json.Net будет повторно использовать существующие значения объектов во время десериализации, а не создавать новые. Таким образом, для таких свойств, как ваш список AttachedTaskLocations, он сначала вызовет метод получения, а затем, найдя существующее значение, продолжит заполнять его из JSON.
  2. Получатель вашего AttachedTaskLocationsне возвращает один и тот же экземпляр каждый раз;он всегда создает новый экземпляр из поля поддержки attachedTaskLocations.

Похоже, происходит следующее:

  1. Сериализатор вызывает геттер AttachedTaskLocations,который возвращает новый пустой список.
  2. Сериализатор заполняет этот список из JSON.
  3. Заполненный список отбрасывается (сериализатор предполагает, что экземпляр AnchorMetaData уже имеет ссылку насписок, так что он никогда не вызывает сеттер).
  4. Когда вы позже получаете доступ к AttachedTaskLocations получателю, он снова возвращает новый пустой список.

Вы можете изменить поведение сериализатораустановив ObjectCreationHandling на Replace. Одно только это изменение, кажется, решает проблему в моем тестировании .

Однако, я думаю, что вы прыгаете через кучу обручей, чтобы 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();
  • добавить его в коллекцию Converters в JsonSerializerSettings и передайте настройки в JsonConvert.SerializeObject() / DeserializeObject() или
  • , добавьте их в коллекцию Converters непосредственно в экземпляре JsonSerializer.

Например:

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

Демонстрация в оба конца: https://dotnetfiddle.net/jmYIq9


Если у вас нет доступа к сериализатору (из вашего вопроса трудно сказать, выполняете ли вы сериализацию/ десериализацию в вашем собственном коде, или обрабатывает ли это какой-то сторонний компонент), тогда другой способ использовать конвертер - через атрибуты. Для свойства списка, такого как AttachedTaskLocations, вы можете указать ItemConverterType прямо в атрибуте [JsonProperty], например:

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

Если бы у вас было одно свойство экземпляра, то вы бы использовали [JsonConverter]атрибут вместо этого:

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

Fiddle: https://dotnetfiddle.net/yxwqDL

...