Подтвердить что ты не робот

Настройка сериализации Json.NET: превращение объекта в массив, чтобы избежать повторения имен свойств

Я отправляю большое количество разных графиков JSON с сервера на клиента (я контролирую оба), и все они содержат патологический случай: большой массив однородных (одинаковых) значений. Так, например, часть полезной нагрузки выглядит так:

[{"LongPropertyName":87, "AnotherVeryLongPropertyName":93,
  "BlahBlahBlahBlahBlah": 78},
 {"LongPropertyName":97, "AnotherVeryLongPropertyName":43,
  "BlahBlahBlahBlahBlah": 578},
 {"LongPropertyName":92, "AnotherVeryLongPropertyName":-3,
  "BlahBlahBlahBlahBlah": 817}, ...

Я добавил некоторое форматирование, но, как вы можете видеть, это смешно с точки зрения Huffman encoding, т.е. что общие вещи должны быть эффективно выражены.

Итак, поскольку я контролирую как десериализацию, так и сериализацию, я хочу реализовать преобразование, где это:

[{"Key1":87,"Key2":99},{"Key1":42,"Key2":-8}]

превращается в нечто вроде этого:

[["$","Key1","Key2"],[87,99],[42,-8]]

который, как вы видите, более компактен даже с двумя объектами.

Где я могу подключиться к Json.NET для этого преобразования? Я хочу сделать это автоматически для максимально возможного количества объектов. Я нашел ContractResolvers, но я не уверен, что они происходят на том этапе, который я хочу - я не уверен, как использовать его методы для превращения JSON объект/словарь в массив.

В качестве альтернативы, если аналогичная вещь уже реализована для Json.NET, я бы хотел использовать ее вместо этого. Но я не смущен тем изменением, которое я хочу сделать (см. Выше), только там, где я бы зацепил Json.NET, чтобы это произошло.

(я пробовал gzipping его, он отлично работает и сбрасывает между 70% и 95%, но он все равно должен выводить полный текст JSON и делать все это сжатие/декомпрессию. Этот вопрос: как я могу просто выводить более компактная форма данных с самого начала?)


Обновление:. Как вы это делаете, с JsonConverter. Я уже написал несколько, но по какой-то причине я думал, что они конфликтуют.

В итоге я основал базу Брайана Роджерса вместе с некоторыми изменениями, чтобы также вставлять/сглаживать любые объекты, которые содержат непосредственно. Это не было частью первоначального вопроса, но причина, по которой я это делал, состоит в том, что если бы у меня было:

[{"A": 42,"B":{"PropOne":87,"PropTwo":93,"PropThree":78}},
{"A":-72,"B":{"PropOne":97,"PropTwo":43,"PropThree":578}]

... Я закончил с:

[["A","B"],[42,{"PropOne":87,"PropTwo":93,"PropThree":78}],
[-72,{"PropOne":97,"PropTwo":43,"PropThree":578}]]

... и это ничего не спасает. Если я вставлял/сплющивал объект в качестве составляющих его ключей, я получаю:

[["A","B_PropOne","B_PropTwo","B_PropThree"],[42,87,93,78],[-72,97,43,578]]
4b9b3361

Ответ 1

Я считаю, что лучший способ добиться того, что вы ищете, - использовать пользовательский JsonConverter, как было предложено @Ilija Dimov. Его конвертер является хорошим началом и должен работать нормально в определенных случаях, но вы можете столкнуться с проблемами, если вы сериализуете более сложный граф объектов. Я предлагаю следующий конвертер в качестве альтернативного решения. Этот преобразователь имеет следующие преимущества:

  • Использует встроенную логику сериализации Json.Net для элементов списка, поэтому любые атрибуты, применяемые к классам, соблюдаются, включая [JsonConstructor] и [JsonProperty]. Другие преобразователи также соблюдаются.
  • Игнорирует списки примитивов и строк, чтобы они были стандартизованы.
  • Поддерживает List<YourClass>, где YourClass содержит сложные объекты, включая List<YourOtherClass>.

Ограничения:

  • В настоящее время не поддерживает списки всех перечислимых, например. List<List<YourClass>> или List<Dictionary<K, YourClass>>, но может быть изменен для этого, если это необходимо. Они будут теперь сериализованы обычным способом.

Вот код для конвертера:

class ListCompactionConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        // We only want to convert lists of non-enumerable class types (including string)
        if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(List<>))
        {
            Type itemType = objectType.GetGenericArguments().Single();
            if (itemType.IsClass && !typeof(IEnumerable).IsAssignableFrom(itemType))
            {
                return true;
            }
        }
        return false;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JArray array = new JArray();
        IList list = (IList)value;
        if (list.Count > 0)
        {
            JArray keys = new JArray();

            JObject first = JObject.FromObject(list[0], serializer);
            foreach (JProperty prop in first.Properties())
            {
                keys.Add(new JValue(prop.Name));
            }
            array.Add(keys);

            foreach (object item in list)
            {
                JObject obj = JObject.FromObject(item, serializer);
                JArray itemValues = new JArray();
                foreach (JProperty prop in obj.Properties())
                {
                    itemValues.Add(prop.Value);
                }
                array.Add(itemValues);
            }
        }
        array.WriteTo(writer);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        IList list = (IList)Activator.CreateInstance(objectType);  // List<T>
        JArray array = JArray.Load(reader);
        if (array.Count > 0)
        {
            Type itemType = objectType.GetGenericArguments().Single();

            JArray keys = (JArray)array[0];
            foreach (JArray itemValues in array.Children<JArray>().Skip(1))
            {
                JObject item = new JObject();
                for (int i = 0; i < keys.Count; i++)
                {
                    item.Add(new JProperty(keys[i].ToString(), itemValues[i]));
                }

                list.Add(item.ToObject(itemType, serializer));
            }
        }
        return list;
    }
}

Ниже приведена полная демоверсия в оба конца с использованием этого конвертера. У нас есть список изменяемых Company объектов, каждый из которых содержит список неизменяемых Employees. Для демонстрационных целей каждая компания также имеет простой список псевдонимов строк, используя собственное имя свойства JSON, и мы также используем IsoDateTimeConverter для настройки формата даты для сотрудника HireDate. Преобразователи передаются в сериализатор через класс JsonSerializerSettings.

class Program
{
    static void Main(string[] args)
    {
        List<Company> companies = new List<Company>
        {
            new Company
            {
                Name = "Initrode Global",
                Aliases = new List<string> { "Initech" },
                Employees = new List<Employee>
                {
                    new Employee(22, "Bill Lumbergh", new DateTime(2005, 3, 25)),
                    new Employee(87, "Peter Gibbons", new DateTime(2011, 6, 3)),
                    new Employee(91, "Michael Bolton", new DateTime(2012, 10, 18)),
                }
            },
            new Company
            {
                Name = "Contoso Corporation",
                Aliases = new List<string> { "Contoso Bank", "Contoso Pharmaceuticals" },
                Employees = new List<Employee>
                {
                    new Employee(23, "John Doe", new DateTime(2007, 8, 22)),
                    new Employee(61, "Joe Schmoe", new DateTime(2009, 9, 12)),
                }
            }
        };

        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.Converters.Add(new ListCompactionConverter());
        settings.Converters.Add(new IsoDateTimeConverter { DateTimeFormat = "dd-MMM-yyyy" });
        settings.Formatting = Formatting.Indented;

        string json = JsonConvert.SerializeObject(companies, settings);
        Console.WriteLine(json);
        Console.WriteLine();

        List<Company> list = JsonConvert.DeserializeObject<List<Company>>(json, settings);
        foreach (Company c in list)
        {
            Console.WriteLine("Company: " + c.Name);
            Console.WriteLine("Aliases: " + string.Join(", ", c.Aliases));
            Console.WriteLine("Employees: ");
            foreach (Employee emp in c.Employees)
            {
                Console.WriteLine("  Id: " + emp.Id);
                Console.WriteLine("  Name: " + emp.Name);
                Console.WriteLine("  HireDate: " + emp.HireDate.ToShortDateString());
                Console.WriteLine();
            }
            Console.WriteLine();
        }
    }
}

class Company
{
    public string Name { get; set; }
    [JsonProperty("Doing Business As")]
    public List<string> Aliases { get; set; }
    public List<Employee> Employees { get; set; }
}

class Employee
{
    [JsonConstructor]
    public Employee(int id, string name, DateTime hireDate)
    {
        Id = id;
        Name = name;
        HireDate = hireDate;
    }
    public int Id { get; private set; }
    public string Name { get; private set; }
    public DateTime HireDate { get; private set; }
}

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

[
  [
    "Name",
    "Doing Business As",
    "Employees"
  ],
  [
    "Initrode Global",
    [
      "Initech"
    ],
    [
      [
        "Id",
        "Name",
        "HireDate"
      ],
      [
        22,
        "Bill Lumbergh",
        "25-Mar-2005"
      ],
      [
        87,
        "Peter Gibbons",
        "03-Jun-2011"
      ],
      [
        91,
        "Michael Bolton",
        "18-Oct-2012"
      ]
    ]
  ],
  [
    "Contoso Corporation",
    [
      "Contoso Bank",
      "Contoso Pharmaceuticals"
    ],
    [
      [
        "Id",
        "Name",
        "HireDate"
      ],
      [
        23,
        "John Doe",
        "22-Aug-2007"
      ],
      [
        61,
        "Joe Schmoe",
        "12-Sep-2009"
      ]
    ]
  ]
]

Company: Initrode Global
Aliases: Initech
Employees:
  Id: 22
  Name: Bill Lumbergh
  HireDate: 3/25/2005

  Id: 87
  Name: Peter Gibbons
  HireDate: 6/3/2011

  Id: 91
  Name: Michael Bolton
  HireDate: 10/18/2012


Company: Contoso Corporation
Aliases: Contoso Bank, Contoso Pharmaceuticals
Employees:
  Id: 23
  Name: John Doe
  HireDate: 8/22/2007

  Id: 61
  Name: Joe Schmoe
  HireDate: 9/12/2009

Я добавил здесь скрипку, если вы хотите сыграть с кодом.

Ответ 2

Вы можете добиться того, чего хотите, используя Custom JsonConverter. Допустим, у вас есть следующий тестовый класс:

public class MyTestClass
{
    public MyTestClass(int key1, string key2, decimal key3)
    {
        m_key1 = key1;
        m_key2 = key2;
        m_key3 = key3;
    }

    private int m_key1;
    public int Key1 { get { return m_key1; } }

    private string m_key2;
    public string Key2 { get { return m_key2; } }

    private decimal m_key3;
    public decimal Key3 { get { return m_key3; } }
}

Это решение предполагает, что вы будете работать с List<MyTestClass> все время, но не привязаны к типу MyTestClass. Это общее решение, которое может работать с любым List<T>, но тип T имеет только свойства и имеет конструктор, который устанавливает все значения свойств.

var list = new List<MyTestClass>
            {
                new MyTestClass
                {
                    Key1 = 1,
                    Key2 = "Str 1",
                    Key3 = 8.3m
                },
                new MyTestClass
                {
                    Key1 = 72,
                    Key2 = "Str 2",
                    Key3 = 134.8m
                },
                new MyTestClass
                {
                    Key1 = 99,
                    Key2 = "Str 3",
                    Key3 = 91.45m
                }
            };

Если вы сериализуете этот список с обычной сериализацией JSON.NET, результатом будет:

[{"Key1":1,"Key2":"Str 1","Key3":8.3},{"Key1":72,"Key2":"Str 2","Key3":134.8},{"Key1":99,"Key2":"Str 3","Key3":91.45}]

Это не то, что вы ожидаете. Из того, что вы разместили, желаемый результат для вас:

[["Key1","Key2","Key3"],[1,"Str 1",8.3],[72,"Str 2",134.8],[99,"Str 3",91.45]]

где первый внутренний массив представляет имена ключей и начиная со второго до последнего - это значения каждого свойства каждого объекта из списка. Вы можете добиться такой сериализации путем написания пользовательского JsonConverter:

public class CustomJsonConverter : JsonConverter
{

    public override bool CanConvert(Type objectType)
    {
        return true;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (!(objectType.IsGenericType)) return null;

        var deserializedList = (IList)Activator.CreateInstance(objectType);
        var jArray = JArray.Load(reader);

        var underlyingType = objectType.GetGenericArguments().Single();
        var properties = underlyingType.GetProperties();

        Type[] types = new Type[properties.Length];

        for (var i = 0; i < properties.Length; i++)
        {
            types[i] = properties[i].PropertyType;
        }

        var values = jArray.Skip(1);

        foreach (JArray value in values)
        {
            var propertiesValues = new object[properties.Length];

            for (var i = 0; i < properties.Length; i++)
            {
                propertiesValues[i] = Convert.ChangeType(value[i], properties[i].PropertyType);
            }

            var constructor = underlyingType.GetConstructor(types);
            var obj = constructor.Invoke(propertiesValues);
            deserializedList.Add(obj);
        }

        return deserializedList;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        if (!(value.GetType().IsGenericType) || !(value is IList)) return;
        var val = value as IList;

        PropertyInfo[] properties = val.GetType().GetGenericArguments().Single().GetProperties();

        writer.WriteStartArray();


        writer.WriteStartArray();

        foreach (var p in properties)
            writer.WriteValue(p.Name);

        writer.WriteEndArray();

        foreach (var v in val)
        {
            writer.WriteStartArray();

            foreach (var p in properties)
                writer.WriteValue(v.GetType().GetProperty(p.Name).GetValue(v));

            writer.WriteEndArray();
        }

        writer.WriteEndArray();
    }
}

и используйте следующую строку для сериализации:

var jsonStr = JsonConvert.SerializeObject(list, new CustomJsonConverter());

Чтобы десериализовать строку в список объектов из typeof(MyTestClass), используйте следующую строку:

var reconstructedList = JsonConvert.DeserializeObject<List<MyTestClass>>(jsonStr, new CustomJsonConverter());

Вы можете использовать CustomJsonConverter с любым общим списком объектов. Обратите внимание, что это решение предполагает, что порядок свойств при сериализации и десериализации одинаковый.

Ответ 3

Manatee.Json может делать прямые преобразования JSON-JSON без беспорядка специальных сериализационных преобразователей. Это первый подход, который использует JSONPath для идентификации конкретных элементов в исходных данных.

Для справки, ваши исходные данные:

[{"Key1":87,"Key2":99},{"Key1":42,"Key2":-8}]

Затем мы определяем шаблон:

[
  ["Key1","Key2"],
  ["$[*]","$.Key1"],
  ["$[*]","$.Key2"]
]

Это приведет к отображению исходных данных:

[["Key1","Key2"],[87,42],[99,-8]]

так, как вы хотели.

Шаблон основан на jsonpath-object-transform. Вот как это работает:

  • По большей части шаблон имеет ту же форму, что и ваша цель.
  • Для каждого свойства вы указываете путь JSON, который идентифицирует данные в источнике. (Отображение свойств объекта прямо не показано в этом примере, так как у вас есть только массивы, но ссылка выше имеет несколько.)
  • Существует специальный случай для массивов. Если массив имеет два элемента, а первый элемент - путь JSON, то второй массив интерпретируется как шаблон для каждого элемента массива. В противном случае массив копируется как есть, сопоставляя данные из источника как обычно, когда элемент является контуром.

Итак, для вашего случая (пропустите комментарии стиля C в JSON),

[                     // Root is an array.
  ["Key1","Key2"],    // Array literal.
  ["$[*]","$.Key1"],  // Take all of the elements in the original array '$[*]'
                      //   and use the value under the "Key1" property '$.Key1'
  ["$[*]","$.Key2"]   // Similiarly for the "Key2" property
]

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

После сопоставления вы можете десериализовать, как вам нравится (Manatee.Json тоже может это сделать для вас.).

Edit

Я понял, что в свой ответ я не ввел код, так что вот оно.

JsonValue source = new JsonArray
    {
        new JsonObject {{"Key1", 87}, {"Key2", 99}},
        new JsonObject {{"Key1", 42}, {"Key2", -8}}
    };
JsonValue template = new JsonArray
    {
        new JsonArray {"Key1", "Key2"},
        new JsonArray {"$[*]", "$.Key1"},
        new JsonArray {"$[*]", "$.Key2"}
    };
var result = source.Transform(template);

Что это.

Изменить 2

У меня возникли проблемы с созданием обратного перевода, поэтому здесь вы можете сделать это только с сериализацией.

Вам нужно будет зарегистрировать несколько методов, чтобы выполнить сопоставление и сериализацию самостоятельно. По сути, вы инструктируете сериализатор, как создавать и разбирать JSON.

Ваша модель данных:

public class MyData
{
    public int Key1 { get; set; }
    public int Key2 { get; set; }
}

Методы сериализации:

public static class MyDataListSerializer
{
    public static JsonValue ToJson(List<MyData> data, JsonSerializer serializer)
    {
        return new JsonArray
            {
                new JsonArray {"Key1", "Key2"},
                new JsonArray(data.Select(d => d.Key1)),
                new JsonArray(data.Select(d => d.Key2)),
            };
    }
    public static MyData FromJson(JsonValue value, JsonSerializer serializer)
    {
        return value.Array.Skip(1)
                    .Array.Select((jv, i) => new MyData
                                             {
                                                 Key1 = (int) jv.Number,
                                                 Key2 = value.Array[2].Array[i]
                                             };
    }
}

Регистрация методов:

JsonSerializationTypeRegistry.RegisterType(MyDataSerializer.ToJson,
                                           MyDataSerializer.FromJson);

И, наконец, методы десериализации. Я не уверен, что такое подписи вашего метода, но вы упомянули, что получаете поток для десериализации, поэтому я начну с этого.

public string Serialize(MyData data)
{
    // _serializer is an instance field of type JsonSerializer
    return _serializer.Serialize(data).ToString();
}
public MyData Deserialize(Stream stream)
{
    var json = JsonValue.Parse(stream);
    return _serializer.Deserialize<MyData>(json);
}

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

Изменить 3

Надеюсь, это последнее редактирование. Этот ответ становится диссертациями.

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

JsonPath указывает альтернативный корневой символ при просмотре элементов массива: @. Это соглашение теперь принято и в трансформаторе.

Исходный шаблон преобразования становится:

[["Key1","Key2"],["$[*]","@.Key1"],["$[*]","@.Key2"]]

Это позволяет создать обратный шаблон:

[
    "$[1][*]",             // Get all of the items in the first value list
    {
        "Key1":"@",        // Key1 is sourced from the item returned by '$[1][*]'
        "Key2":"$[2][*]"   // Key2 is sourced from the items in the second element
                           // of the original source (not the item returned by '$[1][*]')
    }
]

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

Сериализатор будет выглядеть примерно так:

public string Serialize(MyData data)
{
    // _serializer is an instance field of type JsonSerializer
    var json = _serializer.Serialize(data);
    // _transformTemplate is an instance field of type JsonValue
    // representing the first template from above.
    var transformedJson = json.Transform(_transformTemplate);
    return transformedJson.ToString();
}
public MyData Deserialize(Stream stream)
{
    var json = JsonValue.Parse(stream);
    // _reverseTransformTemplate is an instance field of type JsonValue
    // representing the second template from above.
    var untransformedJson = json.Transform(_reverseTransformTemplate);
    return _serializer.Deserialize<MyData>(untransformedJson);
}

Ответ 4

Чтобы ответить на ваш первый вопрос: да, кто-то уже построил это и назвал его 'jsonh'.

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

а затем есть еще один "стандарт", который почти делает это, но предназначен для совершенно одинакового: rjson

И снова: нет С#...

Если вы просто (g) застегнете свои json-данные, он автоматически достигнет желаемого сжатия (но лучше), так как, как вы уже сказали, huffman, он использует дерево huffman. И идея jsonh и rjson заключается в том, чтобы избежать дублирования в ключах, в то время как gzip будет иметь значение между ключами, значениями или другими глифами.

Ответ 5

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

Помимо предложения DoXicK для использования gzip для сжатия, если вы действительно хотите определить другой формат JSON, почему бы просто не преобразовать дерево объектов в С# перед его сериализацией?

Что-то вроде

var input = new[]
    {
        new { Key1 = 87, Key2 = 99 },
        new { Key1 = 42, Key2 = -8 }
    };


var json = JSON.Serialize(Transform(input));


object Transform(object[] input)
{
    var props = input.GetProperties().ToArray();
    var keys = new[] { "$" }.Concat(props.Select(p => p.Name));
    var stripped = input.Select(o => props.Select(p => p.GetValue(o)).ToArray();
    return keys.Concat(stripped);
}

. Таким образом, вы не будете путать программистов, изменив способ работы JSON. Вместо этого преобразование будет явным шагом pre/postprocessing.


*) Я бы даже утверждал, что это похоже на протокол: Object { }, array [ ]. Как сказано в названии, это сериализация вашей структуры объекта. Если вы измените механизм сериализации, вы измените протокол. Как только вы это сделаете, вам больше не нужно будет выглядеть так, как JSON, потому что JSON так или иначе не отражает структуру вашего объекта. Вызов JSON и создание такого вида может потенциально запутать каждого из ваших будущих программистов, а также самого себя, когда вы снова посетите свой код.