JSON десериализация с помощью CustomCreationConverter для создания типа и ввода данных - PullRequest
1 голос
/ 05 августа 2020

У меня есть приложение на основе задач, которому необходимо вводить информацию в некоторые задачи. Задачи могут быть клонированы или сохранены в файле сохранения, в каждом случае класс сериализуется как JSON. Информация о приложении, переданная задачам, не сохраняется, поскольку она сохраняет только сеанс приложения.

public interface IApplicationData { }
public class ApplicationData : IApplicationData { }

public interface ITask {
   IApplicationData Data { get; }
}

[DataContract]
public abstract class Task : ITask, ICloneable {
   protected Task(IApplicationData data = null) {
      Data = data;
   }

   public IApplicationData Data { get; }

   public object Clone() {
      var settings = new JsonSerializerSettings() {
         TypeNameHandling = TypeNameHandling.All
      };
      settings.Converters.Add(new TaskCreator(Data));

      var json = JsonConvert.SerializeObject(this, settings);

      // Reflection equivalent of JsonConvert.DeserializeObject<T>(json, settings);
      var expectedParameters = new Type[] { typeof(string), typeof(JsonSerializerSettings) };
      var method = typeof(JsonConvert).GetMethods().Where(mi => mi.IsGenericMethod && mi.IsStatic && mi.IsPublic && mi.GetParameters().Select(pi => pi.ParameterType).SequenceEqual(expectedParameters)).Single();
      return method.MakeGenericMethod(this.GetType()).Invoke(null, new object[] { json, settings });
   }
}

Задачи могут «соглашаться» на хранение данных приложения или нет, поэтому может выглядеть примерно так:

public class NoDataTask : Task {
   public NoDataTask() { }
}

public class DataTask : Task {
   public DataTask(IApplicationData data) : base(data) { }
}

Я реализовал CustomCreationConverter для создания нового экземпляра соответствующего класса при десериализации из JSON (вы могли заметить, что это используется в реализации Clone() в базе Task класс, показанный выше.

public class TaskCreator : CustomCreationConverter<Task> {
   //public TaskCreator() { } // uncomment to try using converter with JsonProperty attribute in Project

   private readonly IApplicationData _data;
   public TaskCreator(IApplicationData data) {
      _data = data;
   }

   public override Task Create(Type objectType) {
      var hasDataConstructor = objectType.GetConstructor(new Type[] { typeof(IApplicationData) }) != null;
      return hasDataConstructor ? (Task)Activator.CreateInstance(objectType, _data) : (Task)Activator.CreateInstance(objectType);
   }
}

Это работает точно так, как требуется в методе Clone(), полученное objectType относится к DerivedClass (DataTask в приведенном ниже примере)

var data = new ApplicationData();
var dataTask = new DataTask(data);
var dataTaskCloneData = ((DataTask)dataTask.Clone()).Data; // still has data intact - excellent

Однако я не уверен, как это сделать в случае сохранения задач. В настоящее время у меня есть класс Project, содержащий List<ITask>, который я сериализую / де-сериализую. Это отлично работает в отношении данные в каждой задаче, однако мне не удалось ввести ApplicationData в десериализованные экземпляры задач.

[DataContract]
public class Project {
   [DataMember]
   //[JsonProperty(ItemConverterType = typeof(TaskCreator))] // uncomment to force use of converter
   public List<ITask> Tasks { get; set; }
}
var project = new Project {
   Tasks = new List<ITask> {
      new NoDataTask(),
      new DataTask(data)
   }
};

var serialiserSettings = new JsonSerializerSettings {
   TypeNameHandling = TypeNameHandling.All
};
serialiserSettings.Converters.Add(new TaskCreator(data));

var json = JsonConvert.SerializeObject(project, serialiserSettings);
var projectCopy = JsonConvert.DeserializeObject<Project>(json, serialiserSettings);

var projectCopyTask2Data = projectCopy.Tasks[1].Data; // data is null - bad

Я обнаружил, что из-за проекта с List<ITask> преобразователь не используется. Я мог бы добавить преобразователь CustomCreationConverter<ITask>, но в любом случае objectType, переданный преобразователю, всегда имеет тип ITask, тогда как мне нужен производный класс, чтобы иметь возможность создать соответствующий новый экземпляр.

Добавление атрибут [JsonProperty] обеспечивает возможность использования конвертера как есть, но я не знаю метода, который я могу применить без него, используя конструктор без параметров, который бесполезен с учетом моей реализации, поскольку IApplicationData всегда будет быть нулевым.

. NET Пример скрипта здесь - https://dotnetfiddle.net/WdyfDv

1 Ответ

1 голос
/ 06 августа 2020

Мне удалось решить свою проблему, написав свой собственный JsonConverter (в значительной степени основанный на CustomCreationConverter в Newtonsoft.Json.Converters - ссылке GitHub ) следующим образом:

public class TaskCreator : JsonConverter<ITask> {
    private readonly IApplicationData _data;
    public TaskCreator(IApplicationData data) {
        _data = data;
    }

    public override ITask ReadJson(JsonReader reader, Type objectType, [AllowNull] ITask existingValue, bool hasExistingValue, JsonSerializer serializer) {
        if (reader.TokenType == JsonToken.Null) {
            return null;
        }

        // Determine and create the task by reading the type in the JSON
        var jObj = JObject.Load(reader);
        var jsonType = jObj["$type"]?.ToString();
        if (string.IsNullOrWhiteSpace(jsonType)) throw new JsonSerializationException("Cannot determine type of task to create.");
        var type = Type.GetType(jsonType);
        if (type == null) throw new JsonSerializationException($"Could not find the task type {jsonType}");
        var value = Create(type);
        if (value == null) throw new JsonSerializationException("No object created.");

        reader = jObj.CreateReader();
        serializer.Populate(reader, value);
        return value;
    }

    /// <summary>
    /// Creates an object which will then be populated by the serializer.
    /// </summary>
    /// <param name="objectType">Type of the object.</param>
    /// <returns>The created object.</returns>
    public ITask Create(Type objectType) {
        var hasDataConstructor = objectType.GetConstructor(new Type[] { typeof(IApplicationData) }) != null;
        return hasDataConstructor ? (ITask)Activator.CreateInstance(objectType, _data) : (ITask)Activator.CreateInstance(objectType);
    }

    /// <summary>
    /// Writes the JSON representation of the object.
    /// </summary>
    /// <param name="writer">The <see cref="JsonWriter"/> to write to.</param>
    /// <param name="value">The value.</param>
    /// <param name="serializer">The calling serializer.</param>
    public override void WriteJson(JsonWriter writer, [AllowNull] ITask value, JsonSerializer serializer) {
        throw new NotSupportedException($"{ nameof(TaskCreator) } should only be used while deserializing.");
    }

    /// <summary>
    /// Gets a value indicating whether this <see cref="JsonConverter"/> can write JSON.
    /// </summary>
    /// <value>
    ///     <c>true</c> if this <see cref="JsonConverter"/> can write JSON; otherwise, <c>false</c>.
    /// </value>
    public override bool CanWrite => false;
}

'magi c' встречается в ReadJson(), где производный класс ITask извлекается из json '$ type' и создается с использованием отражения. Для этого требуется, чтобы TypeNameHandling был установлен на TypeNameHandling.Objects, что указано в моих настройках сериализатора.

Чтобы использовать это, я могу удалить атрибут JsonProperty из класса Project и убедиться, что JsonSerializerSettings включает конвертер следующим образом:

var data = new ApplicationData("Hello World");
var project = new Project {
    Tasks = new List<ITask> {
        new NoDataTask(),
        new DataTask(data)
    }
};
var serialiserSettings = new JsonSerializerSettings {
    TypeNameHandling = TypeNameHandling.All
};
serialiserSettings.Converters.Add(new TaskCreator(data));
var json = JsonConvert.SerializeObject(project, serialiserSettings);
var projectCopy = JsonConvert.DeserializeObject<Project>(json, serialiserSettings);

Полностью рабочий пример (. NET скрипка) здесь - https://dotnetfiddle.net/Ecrz2S

Я все еще очень открыт к альтернативным подходам, если у кого-то есть что предложить, поскольку это решение все еще кажется мне немного «взломанным».

...