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

我将大量不同的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.NET来进行这种转换? 我想自动为尽可能多的对象执行此操作。 我找到了ContractResolvers,但我不确定它们是否在我想要的阶段发生 – 我不确定如何使用它的方法将JSON对象/字典转换为数组。

或者,如果已经为Json.NET实现了类似的东西,我想要使用它。 但我并不担心我想要做出的那种改变(见上文),就在我将Json.NET挂钩以实现它的地方。

(我试过gzipping它。它工作正常并在70%和95%之间剃掉,但它仍然必须输出完整的JSON文本并完成所有压缩/解压缩。这个问题是:我如何输出更紧凑从一开始的数据forms?)


更新:你这样做的方法是使用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]] 

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

  • 对列表项使用Json.Net的内置序列化逻辑,以便遵守应用于类的任何属性,包括[JsonConstructor][JsonProperty] 。 其他转换器也受到尊重。
  • 忽略基元和字符串列表,以便正常序列化。
  • 支持List ,其中YourClass包含复杂对象,包括List

限制:

  • 目前不支持任何可枚举的List> ,例如List>List> ,但可以根据需要进行修改以执行此操作。 这些将以通常的方式序列化。

这是转换器的代码:

 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 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().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 companies = new List { new Company { Name = "Initrode Global", Aliases = new List { "Initech" }, Employees = new List { 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 { "Contoso Bank", "Contoso Pharmaceuticals" }, Employees = new List { 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 list = JsonConvert.DeserializeObject>(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 Aliases { get; set; } public List 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实现您想要的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只获取属性并且具有设置所有属性值的构造函数。

 var list = new List { 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>(jsonStr, new CustomJsonConverter()); 

您可以将CustomJsonConverter与任何通用的对象列表一起使用。 请注意,此解决方案假定序列化和反序列化期间属性的顺序相同。

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路径,则第二个数组将被解释为数组中每个项的模板。 否则,按原样复制数组,当元素是路径时,正常映射来自源的数据。

所以对于你的情况(原谅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 ] 

注意有一个边缘情况,您希望将值映射到具有两个元素的文字数组。 这将无法正常工作。

映射后,您可以反序列化(Manatee.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 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(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(untransformedJson); } 

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

关于它的坏处是:它不适用于c#,但是你有足够的代码来自己实现它…我还没有看到它作为C#的现成包

然后还有另一个“标准”几乎可以做到这一点,但意思是完全一样: rjson

再说一遍:没有C#……

如果您只是(g)压缩您的json数据,它将自动实现您想要的那种压缩(但更好),因为正如您已经说过的huffman,它使用了一个huffman树。 jsonh和rjson背后的想法是避免键中的重复,而gzip会在键,值或其他字形之间产生差异。

无需自定义JSON转换器 。 只需让你的类实现IEnumerable 。 然后,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 { public string Key1; public string Key2; IEnumerator IEnumerable.GetEnumerator() => EnumerateFields().GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => EnumerateFields().GetEnumerator(); IEnumerable EnumerateFields() { yield return Key1; yield return Key2; } } 

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

 // Base class for objects to be serialized as "[...]" instead of "{...}" public abstract class SerializedAsArray : IEnumerable { IEnumerator IEnumerable.GetEnumerator() => EnumerateFields().GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => EnumerateFields().GetEnumerator(); protected abstract IEnumerable EnumerateFields(); } // will be serialized as: [87,99] public class Foo : SerializedAsArray { public string Key1; public string Key2; protected override IEnumerable 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调用并使其看起来像是这样,当您稍后重新访问代码时,可能会混淆每个/未来的程序员以及您自己。