Невозможно сериализовать словарь с помощью сложного ключа с помощью Json.net

У меня есть словарь с пользовательским типом .net как его ключ. Я пытаюсь сериализовать этот словарь в JSON с помощью JSON.net, но не способен преобразовывать ключи в правильное значение во время сериализации.

class ListBaseClass
{
    public String testA;
    public String testB;
}
-----
var details = new Dictionary<ListBaseClass, string>();
details.Add(new ListBaseClass { testA = "Hello", testB = "World" }, "Normal");
var results = Newtonsoft.Json.JsonConvert.SerializeObject(details);
var data = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<ListBaseClass, string>> results);

Это даст мне → "{\" JSonSerialization.ListBaseClass\ ": \" Normal\ "}"

Однако, если у меня есть свой пользовательский тип как значение в словаре, он хорошо работает

  var details = new Dictionary<string, ListBaseClass>();
  details.Add("Normal", new ListBaseClass { testA = "Hello", testB = "World" });
  var results = Newtonsoft.Json.JsonConvert.SerializeObject(details);
  var data = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, ListBaseClass>>(results);

Это даст мне → "{\" Normal\ ": {\" testA\ ": \" Hello\ ", \" testB\ ": \" World\ "}}"

Может ли кто-нибудь предложить, если я нахожу какое-то ограничение Json.net, или я делаю что-то не так?

Ответы

Ответ 1

В Руководстве по сериализации говорится (см. Раздел: Словари и хеш-таблицы; спасибо @Shashwat за ссылку):

При сериализации словаря ключи словаря преобразуются в строки и используются в качестве имен свойств объекта JSON. Строка, написанная для ключа, может быть настроена путем переопределения ToString() для типа ключа или реализации TypeConverter. TypeConverter также будет поддерживать преобразование пользовательской строки обратно при десериализации словаря.

Я нашел полезный пример того, как реализовать такой конвертер типов на странице с практическими рекомендациями Microsoft:

По сути, мне нужно было расширить System.ComponentModel.TypeConverter и переопределить:

bool CanConvertFrom(ITypeDescriptorContext context, Type source);

object ConvertFrom(ITypeDescriptorContext context,
                   System.Globalization.CultureInfo culture, object value);

object ConvertTo(ITypeDescriptorContext context, 
                 System.Globalization.CultureInfo culture, 
                 object value, Type destinationType);

Также было необходимо добавить атрибут [TypeConverter(typeof(MyClassConverter))] в объявление класса MyClass.

С их помощью я смог автоматически сериализовать и десериализовать словари.

Ответ 2

Вы, вероятно, не хотите использовать ответ, который представил Гордон Бин. Решение работает, но оно предоставляет сериализованную строку для вывода. Если вы используете JSON, это даст вам далеко не идеальный результат, поскольку вы действительно хотите JSON-представление объекта, а не строковое представление.

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

class Point
{
    public int x { get; set; }
    public int y { get; set; }
}

public Dictionary<Point,string> Locations { get; set; };

Используя переопределение TypeConverter, вы получите строковое представление этого объекта при сериализации.

"Locations": {
  "4,3": "foo",
  "3,4": "bar"
},

Но то, что мы действительно хотим, это:

"Locations": {
  { "x": 4, "y": 3 }: "foo",
  { "x": 3, "y": 4 }: "bar"
},

Существует несколько проблем с переопределением TypeConverter для сериализации/десериализации класса.

Во-первых, это не JSON, и вам, возможно, придется написать дополнительную пользовательскую логику, чтобы иметь дело с сериализацией и десериализацией ее в другом месте. (возможно, Javascript в вашем клиентском слое, например?)

Во-вторых, Anywhere, который использует этот объект, теперь будет извергать эту строку, где ранее он правильно сериализовался в объект:

"GridCenterPoint": { "x": 0, "y": 0 },

теперь сериализуется в:

"GridCenterPoint": "0,0",

Вы можете немного управлять форматированием TypeConverter, но не можете избежать того факта, что он отображается как строка, а не как объект.

Эта проблема не проблема с сериализатором, так как json.net прожирает сложные объекты, не пропуская удар, это проблема способа обработки ключей словаря. Если вы попытаетесь взять пример объекта и сериализовать List или даже Hashset, вы заметите, что нет проблем с получением правильного JSON. Это дает нам гораздо более простой способ решения этой проблемы.

В идеале, мы хотели бы просто сказать json.net сериализовать ключ как объект любого типа, а не заставлять его быть строкой. Поскольку это, кажется, не вариант, другой способ - дать json.net что-то, с чем он может работать: List<KeyValuePair<T,K>>.

Если вы передадите список KeyValuePairs в сериализатор json.net, вы получите именно то, что ожидаете. Например, вот гораздо более простая оболочка, которую вы могли бы реализовать:

    private Dictionary<Point, string> _Locations;
    public List<KeyValuePair<Point, string>> SerializedLocations
    {
        get { return _Locations.ToList(); }
        set { _Locations= value.ToDictionary(x => x.Key, x => x.Value); }
    }

Этот прием работает, потому что ключи в kvp не принудительно переводятся в строковый формат. Вы спрашиваете, почему это? Это чертовски бьет меня. объект Dictionary реализует интерфейс IEnumerable<KeyValuePair<TKey, TValue>>, поэтому не должно быть никаких проблем при его сериализации таким же образом, как и в списке kvps, поскольку это, по сути, список kvps. Кто-то (Джеймс Ньютон?) При написании сериализатора словаря Newtonsoft принял решение о том, с какими сложными ключами приходится иметь дело. Вероятно, есть некоторые угловые случаи, которые я не рассматривал, которые делают эту проблему гораздо более сложной.

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

Ответ 3

Все проще

var details = new Dictionary<string, ListBaseClass>();
details.Add("Normal", new ListBaseClass { testA = "Hello", testB = "World" });
var results = Newtonsoft.Json.JsonConvert.SerializeObject(details.ToList());
var data = 
Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<ListBaseClass, string>> results);

Образец

 class Program
{
    static void Main(string[] args)
    {
        var  testDictionary = new Dictionary<TestKey,TestValue>()
        {
            {
                new TestKey()
                {
                    TestKey1 = "1",
                    TestKey2 = "2",
                    TestKey5 = 5
                },
                new TestValue()
                {
                    TestValue1 = "Value",
                    TestValue5 = 96
                }
            }
        };

        var json = JsonConvert.SerializeObject(testDictionary);
        Console.WriteLine("=== Dictionary<TestKey,TestValue> ==");
        Console.WriteLine(json);
        // result: {"ConsoleApp2.TestKey":{"TestValue1":"Value","TestValue5":96}}


        json = JsonConvert.SerializeObject(testDictionary.ToList());
        Console.WriteLine("=== List<KeyValuePair<TestKey, TestValue>> ==");
        Console.WriteLine(json);
        // result: [{"Key":{"TestKey1":"1","TestKey2":"2","TestKey5":5},"Value":{"TestValue1":"Value","TestValue5":96}}]


        Console.ReadLine();

    }
}

class TestKey
{
    public string TestKey1 { get; set; }

    public string TestKey2 { get; set; }

    public int TestKey5 { get; set; }
}

class TestValue 
{
    public string TestValue1 { get; set; }

    public int TestValue5 { get; set; }
}