自定义Json.. NET序列化:将对象转换为数组以避免属性名称的重复

本文关键字:属性 数组 序列化 NET Json 转换 对象 自定义 | 更新日期: 2023-09-27 18:17:54

我正在将大量不同的JSON图从服务器发送到客户机(两者都由我控制),它们都包含一个病态情况:大量同质(相同类型)值。因此,例如,部分有效负载看起来像:

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

我已经添加了一些格式,但正如你所看到的,从霍夫曼编码的角度来看,这是荒谬的,即常见的东西应该被有效地表达。

因此,由于我控制了反序列化和序列化结束,因此我想实现一个转换,其中:

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

会变成这样:

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

你可以看到,即使只有两个对象,它也更紧凑。

我在哪里钩到Json。做这个转换?我想为尽可能多的对象自动执行此操作。我已经找到了ContractResolvers,但我不确定它们是否在我想要的阶段发生-我不确定如何使用它的方法将JSON对象/字典转换为数组。

或者,如果Json已经实现了类似的事情。NET,我想用它来代替。但我并不困惑于我想要做的那种改变(见上文),只是在哪里我要钩到Json。. NET来实现。

我试过压缩它。它工作得很好,在70%到95%之间,但它仍然需要输出完整的JSON文本并进行所有的压缩/解压缩。这个问题是:我如何从一开始就输出一个更紧凑的数据形式?

Update:您这样做的方式是使用JsonConverter。我已经写了几篇,但由于某种原因,我认为它们会冲突。

我最终得到的是Brian Rogers的基础以及一些变化,也嵌入/平坦任何直接包含的对象。这不是最初问题的一部分,但我这样做的原因是,如果我有:

[{"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]]

自定义Json.. NET序列化:将对象转换为数组以避免属性名称的重复

我相信实现您正在寻找的最好方法是使用@Ilija Dimov建议的自定义JsonConverter。他的转换器是一个很好的开始,在某些情况下应该可以很好地工作,但如果序列化更复杂的对象图,可能会遇到麻烦。我提供以下转换器作为替代解决方案。该转换器具有以下优点:

  • 使用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

我在这里添加了一个提琴,以防您想摆弄代码

您可以通过使用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只有get属性,并且有一个设置所有属性值的构造函数。

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用于任何泛型对象列表。请注意,此解决方案假定序列化和反序列化期间的属性顺序相同。

海牛。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 Path来标识源中的数据。(对象属性映射没有直接显示在这个例子中,因为你只有数组,但上面的链接有几个。)
  • 数组有一个特殊情况。如果数组有两个元素,并且第一个元素是一个JSON Path,那么第二个数组将被解释为数组中每个项目的模板。否则,将按原样复制数组,当元素为路径时,将照常从源映射数据。

对于你的情况(原谅JSON中c风格的注释),

[                     // 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
]

注意有一个边缘情况,您想要将值映射到具有两个元素的文字数组。这将不能正常工作。

一旦映射,你可以反序列化任何你喜欢的(海牛。Json也可以为你做这些。

编辑

我意识到我没有在我的答案中加入任何代码,所以这里是。

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

回答你的第一个问题:是的,有人已经构建了这个,并称之为'jsonh'。

它的缺点是:它不能用于c#,但你有足够的代码来实现它自己…我还没有看到它作为c#任何地方的现成包

,然后还有另一个"标准"几乎做到了这一点,但意思完全相同:rjson

如果你只是(g)压缩你的json数据,它会自动实现你想要的那种压缩(但更好),因为,正如你已经说过的霍夫曼,它使用霍夫曼树。jsonh和rjson背后的思想是避免键中的重复,而gzip会在键、值或其他符号之间产生差异。

无需自定义JSON转换器。只要让你的类实现IEnumerable<object>。Json。. NET将把你的数据序列化为数组而不是对象。

例如,不是…

// will be serialized as: {"Key1":87,"Key2":99}
public class Foo
{
    public string Key1;
    public string Key2;
}

…写这:

// will be serialized as: [87,99]
public class Foo : IEnumerable<object>
{
    public string Key1;
    public string Key2;
    IEnumerator<object> IEnumerable<object>.GetEnumerator() => EnumerateFields().GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => EnumerateFields().GetEnumerator();
    IEnumerable<object> EnumerateFields()
    {
        yield return Key1;
        yield return Key2;
    }
}

如果您需要将此策略应用于许多类,那么您可以声明一个抽象基类来摆脱一些样板文件:

// Base class for objects to be serialized as "[...]" instead of "{...}"
public abstract class SerializedAsArray : IEnumerable<object>
{
    IEnumerator<object> IEnumerable<object>.GetEnumerator() =>
        EnumerateFields().GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() =>
        EnumerateFields().GetEnumerator();
    protected abstract IEnumerable<object> EnumerateFields();
}
// will be serialized as: [87,99]
public class Foo : SerializedAsArray
{
    public string Key1;
    public string Key2;
    protected override IEnumerable<object> EnumerateFields()
    {
        yield return Key1;
        yield return Key2;
    }
}

流行的JSON序列化库(更不用说JSON背后的全部思想)的一大优点是采用语言特性——对象、数组、文字——并将它们序列化为等效的JSON表示。你可以看看c#中的对象结构(例如),就知道JSON是什么样子的。如果您开始更改整个序列化机制,情况就不是这样了。*)

除了DoXicK建议使用gzip进行压缩之外,如果你真的想要定义一个不同的JSON格式,为什么不在序列化它之前简单地在c#中转换你的对象树呢?

之类的
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的工作方式而使任何程序员感到困惑。相反,转换将是一个显式的预处理/后处理步骤。


*)我甚至认为它就像一个协议:对象是{ },数组是[ ]。顾名思义,它是对象结构的序列化。如果你改变了序列化机制,你就改变了协议。一旦这样做了,就不需要再像JSON那样了,因为JSON无论如何都不能正确地表示对象结构。将其称为JSON并使其看起来像JSON有可能使您的每个同事/未来的程序员以及您自己在以后重新访问代码时感到困惑。