自定义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]]
我相信实现您正在寻找的最好方法是使用@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有可能使您的每个同事/未来的程序员以及您自己在以后重新访问代码时感到困惑。