Json.Net Как сериализовать и десериализовать пользовательские типы по-своему

Я использую библиотеку реактивного программирования R3 и Newtonsoft. Я пытаюсь сохранить данные объекта в файл, чтобы позже можно было их прочитать. Запись просто работает без проблем. Но при чтении файла выдает ошибку Newtonsoft.Json.JsonReaderException: Unexpected character encountered while parsing value: {. Path 'resourceList[0].Count', line 5, position 16.

{
  "resourceList": [
    {
      "Name": "banana",
      "Count": {
        "EqualityComparer": {},
        "CurrentValue": 10,
        "HasObservers": false,
        "IsCompleted": false,
        "IsDisposed": false,
        "IsCompletedOrDisposed": false,
        "Value": 10
      }
    },
    {
      "Name": "apple",
      "Count": {
        "EqualityComparer": {},
        "CurrentValue": 3,
        "HasObservers": false,
        "IsCompleted": false,
        "IsDisposed": false,
        "IsCompletedOrDisposed": false,
        "Value": 3
      }
    }
  ],
  "observableResourceList": [
    {
      "Name": "oBanana",
      "Count": {
        "EqualityComparer": {},
        "CurrentValue": 5,
        "HasObservers": false,
        "IsCompleted": false,
        "IsDisposed": false,
        "IsCompletedOrDisposed": false,
        "Value": 5
      }
    },
    {
      "Name": "oApple",
      "Count": {
        "EqualityComparer": {},
        "CurrentValue": 2,
        "HasObservers": false,
        "IsCompleted": false,
        "IsDisposed": false,
        "IsCompletedOrDisposed": false,
        "Value": 2
      }
    }
  ],
  "number": {
    "EqualityComparer": {},
    "CurrentValue": 9,
    "HasObservers": false,
    "IsCompleted": false,
    "IsDisposed": false,
    "IsCompletedOrDisposed": false,
    "Value": 9
  },
  "word": {
    "EqualityComparer": {},
    "CurrentValue": "word string",
    "HasObservers": false,
    "IsCompleted": false,
    "IsDisposed": false,
    "IsCompletedOrDisposed": false,
    "Value": "word string"
  }
}

Также в файл попадает много ненужной информации из ReactiveProperty, я стараюсь сохранять только ту часть, которая мне нужна. Мне удалось записать часть файла с помощью JsonConverter, но я все еще не могу его правильно прочитать. Я получаю ту же ошибку

{
  "resourceList": [
    {
      "Name": "banana",
      "Count": {
        "Value": 10
      }
    },
    {
      "Name": "apple",
      "Count": {
        "Value": 3
      }
    }
  ],
  "observableResourceList": [
    {
      "Name": "oBanana",
      "Count": {
        "Value": 5
      }
    },
    {
      "Name": "oApple",
      "Count": {
        "Value": 2
      }
    }
  ],
  "number": {
    "Value": 9
  },
  "word": {
    "Value": "word string"
  }
}

Кроме того, я не понимаю, как описать определение обобщенного типа типа ReactiveProperty<> в Json.NET, неясно, как интерпретировать JsonConverter, ContractResolver или что-то в этом роде.

public class Test : MonoBehaviour
{
    private void Start()
    {
        TestData testData = new TestData();

        DataStorageAgent dataStorageAgent = new DataStorageAgent();
        string savePath = Application.dataPath + "/Saves/Test.json";

        dataStorageAgent.Save(testData, savePath);
        testData = dataStorageAgent.Load<TestData>(savePath);
    }
}
public class TestData
{
    public List<Resource> resourceList;
    public ObservableList<Resource> observableResourceList;
    public ReactiveProperty<int> number;
    public ReactiveProperty<string> word;

    public TestData()
    {
        resourceList = new List<Resource>();
        resourceList.Add(new Resource("banana", 10));
        resourceList.Add(new Resource("apple", 3));

        observableResourceList = new ObservableList<Resource>();
        observableResourceList.Add(new Resource("oBanana", 5));
        observableResourceList.Add(new Resource("oApple", 2));

        number = new ReactiveProperty<int>(9);
        word = new ReactiveProperty<string>("word string");
    }
}
public class Resource
{
    public readonly string Name;
    public ReactiveProperty<int> Count;

    public Resource(string name, int count)
    {
        Name = name;
        Count = new ReactiveProperty<int>(count);
    }
}
public class DataStorageAgent
{
    public T Load<T>(string saveFilePath) where T : class
    {
        if (File.Exists(saveFilePath))
        {
            string jsonString = File.ReadAllText(saveFilePath);

            try
            {
                return JsonConvert.DeserializeObject<T>(jsonString, new ReactivePropertyJsonConverter());
            }
            catch (Exception e)
            {
                Debug.LogError(e);
            }
        }
        else
        {
            Debug.LogError($"File does not exist - {saveFilePath}");
        }

        return default(T);
    }

    public void Save(object saveData, string saveFilePath)
    {
        string jsonString = JsonConvert.SerializeObject(saveData, Formatting.Indented, new ReactivePropertyJsonConverter());
        File.WriteAllText(saveFilePath, jsonString);
    }
}
public class ReactivePropertyJsonConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType.IsGenericType && (objectType.GetGenericTypeDefinition() == typeof(ReactiveProperty<>));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        //JObject jsonObject = JObject.Load(reader);
        //return existingValue;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        writer.WriteStartObject();
        writer.WritePropertyName("Value");
        serializer.Serialize(writer, value.GetType().GetProperty("Value").GetValue(value));
        writer.WriteEndObject();
    }
}

Что такое Р3? Это github.com/Cysharp/R3 ? Если да, то находится ли ReactiveProperty<T> здесь: github.com/Cysharp/R3/blob/main/src/R3/ReactiveProperty.cs? И если да, то что вы хотите делать с сериализацией и десериализацией IEqualityComparer<T>? equalityComparer? Просто использовать значение по умолчанию при десериализации? Что-то другое?

dbc 19.07.2024 19:30

Да это оно. Теперь меня интересует только значение. Короткий json в посте — это то, что я хочу получить в данный момент. Просто - Подсчет: Значение

Maers 19.07.2024 19:32

При десериализации я пытаюсь прочитать значение и заменить его в реактивном свойстве.

Maers 19.07.2024 19:43

Итак, вы хотите игнорировать компаратор равенства при сериализации и использовать значение по умолчанию при десериализации?

dbc 19.07.2024 19:46

Да, я пытаюсь сделать что-то подобное »:)

Maers 19.07.2024 19:53
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
5
100
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы пытаетесь создать один собственный JsonConverter для ReactiveProperty<T> для каждого возможного типа T, который будет сериализовать реактивное свойство как его значение . Я предлагаю разбить это на два этапа. Сначала создайте общий JsonConverter<ReactiveProperty<T>>, который можно использовать для любого конкретного типа значения T. Во-вторых, создайте общий преобразователь для всех возможных типов значений, который использует отражение для создания и вызова преобразователей для каждого типа.

Общий JsonConverter<ReactiveProperty<T>> будет выглядеть следующим образом:

public class ReactivePropertyJsonConverter<T> : JsonConverter<ReactiveProperty<T>>
{
    public override ReactiveProperty<T>? ReadJson(JsonReader reader, Type objectType, ReactiveProperty<T>? existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        var isNull = reader.MoveToContentAndAssert().TokenType == JsonToken.Null;
        if (isNull && typeof(T).IsValueType && Nullable.GetUnderlyingType(typeof(T)) == null)
            return null;
        var innerValue = !isNull ? serializer.Deserialize<T>(reader) : default(T);
        if (hasExistingValue && existingValue != null)
            existingValue.Value = (innerValue ?? default(T))!;
        else if (objectType == typeof(ReactiveProperty<T>))
            existingValue = innerValue == null ? new() : new(innerValue!);
        else
            // ReactiveProperty<T> was subclassed, use parameterized constructor.
            existingValue = innerValue == null 
                ? (ReactiveProperty<T>)Activator.CreateInstance(objectType)!
                // Here we assume the subclass has a parameterized constructor taking the value as a a single argument.
                : (ReactiveProperty<T>)Activator.CreateInstance(objectType, innerValue)!;
        return existingValue;
    }

    public override void WriteJson(JsonWriter writer, ReactiveProperty<T>? value, JsonSerializer serializer) => 
        serializer.Serialize(writer, value == null ? default(T?) : value.Value);
}

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;
    }
}

Метод WriteJson() прост, но в отношении ReadJson() следует отметить пару моментов:

  • Когда передается предварительно выделенный existingValue, я заменяю его значение, а не создаю новое реактивное свойство.

  • ReactiveProperty<T> не запечатан, поэтому objectType может быть подклассом некоторого ReactiveProperty<T>. В этом случае нам придется использовать Activator.CreateInstance() для создания экземпляров этого типа.

Теперь что касается общего преобразователя, мы можем использовать Type.MakeGenericType(Type[]) для создания конкретного типа JsonConverter<ReactiveProperty<T>> для каждого типа значения T:

public class ReactivePropertyJsonConverter : JsonConverter
{
    static readonly ConcurrentDictionary<Type, JsonConverter> Converters = new();

    static JsonConverter CreateReactivePropertyConverterOfT(Type valueType) =>
        (JsonConverter)Activator.CreateInstance(typeof(ReactivePropertyJsonConverter<>).MakeGenericType(valueType))!;

    static JsonConverter GetReactivePropertyConverterOfT(Type valueType) =>
        Converters.GetOrAdd(valueType, CreateReactivePropertyConverterOfT);

    static Type? GetReactivePropertyValueType(Type objectType) =>
        objectType.BaseTypesAndSelf().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ReactiveProperty<>)).FirstOrDefault()?.GetGenericArguments()[0];
    
    public override bool CanConvert(Type objectType) => GetReactivePropertyValueType(objectType) != null;

    public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) => 
        GetReactivePropertyConverterOfT(GetReactivePropertyValueType(objectType)!).ReadJson(reader, objectType, existingValue, serializer);
    
    public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
    {
        if (value == null)
            writer.WriteNull();
        else
            GetReactivePropertyConverterOfT(GetReactivePropertyValueType(value.GetType())!).WriteJson(writer, value, serializer);
    }
}

public static partial class JsonExtensions
{
    public static IEnumerable<Type> BaseTypesAndSelf(this Type type)
    {
        while (type != null)
        {
            yield return type;
            type = type.BaseType!;
        }
    }
}

Теперь вы можете изменить DataStorageAgent, чтобы использовать конвертер, например. следующее:

public class DataStorageAgent
{
    static UTF8Encoding DefaultEncoding { get; } = new UTF8Encoding(false, true);   
    
    static JsonSerializerSettings DefaultSettings { get; } = new ()
    {
        Converters = { new ReactivePropertyJsonConverter() },
        Formatting = Formatting.Indented,
    };

    public T? Load<T>(string saveFilePath) where T : class
    {
        try
        {
            using var textReader = new StreamReader(saveFilePath, DefaultEncoding);
            using var jsonReader = new JsonTextReader(textReader);
            return JsonSerializer.CreateDefault(DefaultSettings).Deserialize<T>(jsonReader);
        }
        catch (Exception ex)
        {
            Debug.LogError(ex);
            return default(T);
        }
    }

    public void Save(object saveData, string saveFilePath)
    {
        using var textWriter = new StreamWriter(saveFilePath, false, DefaultEncoding);
        JsonSerializer.CreateDefault(DefaultSettings).Serialize(textWriter, saveData);
    }
}

А также измените TestData, чтобы не заполнять его списки в конструкторе следующим образом:

public class TestData
{
    public List<Resource> resourceList = new();
    public ObservableList<Resource> observableResourceList = new();
    public ReactiveProperty<int> number = new ();
    public ReactiveProperty<string> word = new ();

    public TestData()  { }

    public static TestData Create() =>
        new ()
        {
            resourceList = { new Resource("banana", 10), new Resource("apple", 3) },
            observableResourceList = { new Resource("oBanana", 5), new Resource("oApple", 2) },
            number = new ReactiveProperty<int>(9),
            word = new ReactiveProperty<string>("word string"),
        };
}

И у вас должно быть все готово.

Примечания:

  • Type.MakeGenericType() и Activator.CreateInstance() могут быть немного медленными, поэтому в ReactivePropertyJsonConverter я использовал параллельный словарь для кэширования и повторного использования экземпляров конвертера.

  • Кажется, я припоминаю, что на некоторых платформах (например, IOS) работа Type.GenericMakeGenericType() не гарантируется. Если вы используете эти платформы, ReactivePropertyJsonConverter не будет работать, поэтому вам придется вручную создать все необходимые ReactivePropertyJsonConverter<T> конвертеры и добавить их в список JsonSerializerSettings.Converters.

  • По умолчанию при десериализации предварительно выделенного свойства List<T> Json.NET добавит десериализованные элементы в список. Таким образом, если список уже был заполнен в каком-то конструкторе, содержимое списка может удвоиться.

    Чтобы избежать этого, я изменил ваш TestData для заполнения списков в методе Create() и сделал var testData = TestData.Create(); в тестовом методе, но если вам нужно заполнить списки внутри конструкторов, вам нужно будет использовать ObjectCreationHandling.Replace, как показано в JSON.NET. Почему это так Добавить в список вместо перезаписи?.

  • В разделе Советы по производительности: оптимизируйте использование памяти Newtonsoft рекомендует десериализовать файлы JSON непосредственно из файловых потоков, а не загружать содержимое в строки с последующей десериализацией строк.

Демо-рабочий пример здесь.

Ух ты! Выглядит действительно впечатляюще. Это именно то, чего я пытался достичь. Это оказалось сложнее, чем я думал. Большое спасибо за предоставление этого решения! Я уже потратил много времени на изучение вопроса и, похоже, потратил бы гораздо больше, если бы не ваша помощь.

Maers 19.07.2024 23:24

Другие вопросы по теме