Десериализовать в существующий экземпляр классов в C#

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

Я пытаюсь найти способ сериализации класса с его идентификатором, а затем десериализации обратно к его полному описанию. Например, если у нас есть такая структура города:

public struct City
{
    public string Name;
    public Country Country;
}

[JsonObject(MemberSerialization.OptIn)]
public class Country
{
    public Country(string code, string name) { Code = code; Name = name; }
    [JsonProperty]
    public string Code;
    public string Name;
}

public class Countries
{
    public static List<Country> All = new List<Country>()
    {
        new Country("US", "United States"),
        new Country("GB", "United Kingdom"),
        new Country("FR", "France"),
        new Country("ES", "Spain"),
    };
}

Я не против страны, сериализованной как {"code":"ES"} или просто "ES", но я бы хотел, чтобы она была десериализована как существующий экземпляр страны в странах. Все

Как я мог получить это поведение?

2 ответа

Решение

Я бы порекомендовал использовать JsonConverter вот так:

public class CountryConverter : JsonConverter
{
    public override bool CanRead { get { return true; } }
    public override bool CanWrite { get { return false; } }

    public override bool CanConvert(Type objectType)
    {
        return typeof(Country) == objectType;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var obj = JObject.Load(reader);
        var code = obj.GetValue("code", StringComparison.OrdinalIgnoreCase)?.Value<string>();
        if (code != null)
        {
            return Countries.All.FirstOrDefault(c => string.Equals(c.Code, code, StringComparison.OrdinalIgnoreCase));
        }
        return null;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

Он не берет на себя ответственность за сериализацию объектов, а только за их десериализацию. Чтобы десериализовать их, он читает поле "код", а затем возвращает первую соответствующую страну из Countries.All список. Вероятно, было бы лучше (более эффективно) использовать вместо этого словарь.

Чтобы использовать это, просто украсьте свой класс страны так:

[JsonConverter(typeof(CountryConverter))]
public class Country

У вас есть два решения:

  • Используйте перечисления и функции расширения, которые упростили бы процесс проверки, так как перечисления ограничены конкретно значениями
  • Используйте пользовательские JsonConverters для преобразования ваших данных JSON с помощью идентификаторов / кода, что позволит вам настроить их сериализацию в JSON.

У меня скоро будут примеры, мне нужно набрать их до конца. Изменить: примеры завершены


В обоих случаях
В обоих примерах я использовал JsonConvertAttribute, чтобы указать, какой конвертер следует использовать при сериализации или десериализации объекта. Этот параметр сообщает библиотеке json.net, какой класс / конвертер использовать при сериализации объекта / параметра по умолчанию.

Если вам нужна сериализация только в определенные моменты, у вас есть варианты для 2 разных сценариев:

  • При сериализации массива / списка
    • Добавьте атрибут JsonPropertyAttribute к свойству и установите для ItemConverterType тип вашего конвертера.
  • При сериализации чего-либо еще
    • Добавьте атрибут JsonConvertAttribute к свойству и в конструкторе передайте преобразователь JSON.

Сохранение страны для объекта / структуры
Этот вариант, на мой взгляд, является наиболее гибким и практичным из 2-х решений, поскольку он позволяет изменять требования и уменьшать количество исключений.

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyApplication
{

    public struct City
    {
        public string Name;
        // you could also set a converter for this field specifically if you only need for specific fields but also
        // still want it to display as a normal json object when you serialise the object.
        // [JsonConverter(typeof(CountryConverter))]
        public Country Country;
    }

    // Setting a json converter attribute allows json.net to understand that an object by default
    // will be serialised and deserialised using the specified converter.
    [JsonConverter(typeof(CountryConverter))]
    public class Country
    {
        public Country(string code)
        {
            switch (code)
            {
                case "US": Name = "United-States"; break;
                case "GB": Name = "United Kingdom"; break;
                case "FR": Name = "France"; break;
                case "ES": Name = "Spain"; break;
                case "CA": Name = "Canada"; break;
            }
        }
        public string Code { get; set; }
        public string Name { get; set; }
    }


    public class CountryConverter : JsonConverter<Country>
    {
        // Assuming that the countries are serialised using the code
        public override Country ReadJson(JsonReader reader, Type objectType, Country existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            if(reader.Value == null && reader.ValueType == typeof(string))
            {
                return null;
            }

            string code = (string) reader.Value;
            code = code.ToUpperInvariant(); // Because reducing error points is usually a good thing

            return new Country(code);

        }

        public override void WriteJson(JsonWriter writer, Country value, JsonSerializer serializer)
        {
            //Writes the code as the value for the object
            writer.WriteValue(value.Code);
        }
    }
}

Изменение страны в перечисление
Использование enum может быть полезным, поскольку оно позволяет вам иметь набор неизменяемых значений, которым не нужно создавать новые объекты снова и снова. Но это делает вашу логику преобразования немного более сложной.

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyApplication
{

    public struct City
    {
        public string Name;
        public Country Country;
    }

    // Using the full name as it makes it easier to work with and also because you'd need a json converter
    // if you want a string and not the number/index of the country when you serialise the data
    [JsonConverter(typeof(CountryConverter))]
    public enum Country
    {
        UnitedStates,
        UnitedKingdom,
        France,
        Spain,
        Canada
    }

    // this class only exists for you to add extentions to this enums.
    // In short extentions are a type of methods added to other classes that act
    // as if they were part of outher classes. usually this means that the first parameter
    // is prefixed by this.
    public static class CountryExtentions
    {
        public static string GetCode(this Country country)
        {
            switch (country)
            {
                case Country.UnitedStates: return "US";
                case Country.UnitedKingdom: return "GB";
                case Country.France: return "FR";
                case Country.Spain: return "SP";
                case Country.Canada: return "CA";
                default: throw new InvalidOperationException($"This country has no code {country.ToString()}");
            }
        }
    }
    public class CountryConverter : JsonConverter<Country>
    {
        // Assuming that the countries are serialised using the code
        public override Country ReadJson(JsonReader reader, Type objectType, Country existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            // make sure you can convert the thing into a string
            if (reader.Value == null && reader.ValueType == typeof(string))
            {
                throw new InvalidOperationException($"The data type passed {reader.ValueType.Name} isn't convertible. The data type musts be a string.");
            }

            // get the value
            string code = (string)reader.Value;
            code = code.ToUpperInvariant(); // Because reducing error points is usually a good thing

            // cycle through the enum values to compare them to the code
            foreach (Country country in Enum.GetValues(typeof(Country)))
            {
                // if the code matches
                if (country.GetCode() == code)
                {
                    // return the country enum
                    return country;
                }
            }
            // if no match is found, the code is invlalid
            throw new InvalidCastException("The provided code could not be converted.");

        }

        public override void WriteJson(JsonWriter writer, Country value, JsonSerializer serializer)
        {
            //Writes the code as the value for the object
            writer.WriteValue(value.GetCode());
        }
    }
}
Другие вопросы по тегам