Json.NET,如何自定义序列化以插入JSON属性 [英] Json.NET, how to customize serialization to insert a JSON property
问题描述
我无法找到JsonConvert.WriteJson
的合理实现,该实现允许我在序列化特定类型时插入JSON属性.我所有的尝试都导致了"JsonSerializationException:检测到类型为XXX的自引用循环".
I have been unable to find a reasonable implementation for JsonConvert.WriteJson
that allows me to insert a JSON property when serializing specific types. All my attempts have resulted in "JsonSerializationException : Self referencing loop detected with type XXX".
我要解决的问题的一些背景知识:我正在使用JSON作为配置文件格式,并且正在使用JsonConverter
来控制我的配置类型的类型解析,序列化和反序列化.我想使用更有意义的JSON值来解析正确的类型,而不是使用$type
属性.
A little more background on the problem I'm trying to solve: I am using JSON as a config file format, and I'm using a JsonConverter
to control the type resolution, serialization, and deserialization of my configuration types. Instead of using the $type
property, I want to use more meaningful JSON values that are used to resolve the correct types.
在我的精简示例中,这是一些JSON文本:
In my pared-down example, here's some JSON text:
{
"Target": "B",
"Id": "foo"
}
,其中JSON属性"Target": "B"
用于确定该对象应序列化为B
类型.举这个简单的例子,这种设计似乎并不引人注目,但它确实使配置文件格式更有用.
where the JSON property "Target": "B"
is used to determine that this object should be serialized into type B
. This design might not seem that compelling given the simple example, but it does make the config file format more usable.
我还希望配置文件是可双向访问的.我有反序列化的情况下工作,我不能工作的是序列化的情况下.
I also want the config files to be round-trippable. I have the deserialize case working, what I can't get working is the serialize case.
我的问题的根源是我找不到使用标准JSON序列化逻辑的JsonConverter.WriteJson
实现,并且没有引发自我引用循环"异常.这是我的实现:
The root of my problem is that I can't find an implementation of JsonConverter.WriteJson
that uses the standard JSON serialization logic, and doesn't throw a "Self referencing loop" exception. Here's my implementation:
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JProperty typeHintProperty = TypeHintPropertyForType(value.GetType());
//BUG: JsonSerializationException : Self referencing loop detected with type 'B'. Path ''.
// Same error occurs whether I use the serializer parameter or a separate serializer.
JObject jo = JObject.FromObject(value, serializer);
if (typeHintProperty != null)
{
jo.AddFirst(typeHintProperty);
}
writer.WriteToken(jo.CreateReader());
}
在我看来,这似乎是Json.NET中的错误,因为应该有一种方法可以做到这一点.不幸的是,我遇到的所有JsonConverter.WriteJson
示例(例如在JSON.NET中对特定对象的自定义转换)仅提供特定类的自定义序列化,使用JsonWriter方法写出单个对象和属性.
The seems to me to be a bug in Json.NET, because there should be a way to do this. Unfortunately all the examples of JsonConverter.WriteJson
that I've come across (eg Custom conversion of specific objects in JSON.NET) only provide custom serialization of a specific class, using the JsonWriter methods to write out individual objects and properties.
这是展示我问题的xunit测试的完整代码(或在此处查看)
Here's the complete code for an xunit test that exhibits my problem (or see it here )
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using Xunit;
public class A
{
public string Id { get; set; }
public A Child { get; set; }
}
public class B : A {}
public class C : A {}
/// <summary>
/// Shows the problem I'm having serializing classes with Json.
/// </summary>
public sealed class JsonTypeConverterProblem
{
[Fact]
public void ShowSerializationBug()
{
A a = new B()
{
Id = "foo",
Child = new C() { Id = "bar" }
};
JsonSerializerSettings jsonSettings = new JsonSerializerSettings();
jsonSettings.ContractResolver = new TypeHintContractResolver();
string json = JsonConvert.SerializeObject(a, Formatting.Indented, jsonSettings);
Console.WriteLine(json);
Assert.Contains(@"""Target"": ""B""", json);
Assert.Contains(@"""Is"": ""C""", json);
}
[Fact]
public void DeserializationWorks()
{
string json =
@"{
""Target"": ""B"",
""Id"": ""foo"",
""Child"": {
""Is"": ""C"",
""Id"": ""bar"",
}
}";
JsonSerializerSettings jsonSettings = new JsonSerializerSettings();
jsonSettings.ContractResolver = new TypeHintContractResolver();
A a = JsonConvert.DeserializeObject<A>(json, jsonSettings);
Assert.IsType<B>(a);
Assert.IsType<C>(a.Child);
}
}
public class TypeHintContractResolver : DefaultContractResolver
{
public override JsonContract ResolveContract(Type type)
{
JsonContract contract = base.ResolveContract(type);
if ((contract is JsonObjectContract)
&& ((type == typeof(A)) || (type == typeof(B))) ) // In the real implementation, this is checking against a registry of types
{
contract.Converter = new TypeHintJsonConverter(type);
}
return contract;
}
}
public class TypeHintJsonConverter : JsonConverter
{
private readonly Type _declaredType;
public TypeHintJsonConverter(Type declaredType)
{
_declaredType = declaredType;
}
public override bool CanConvert(Type objectType)
{
return objectType == _declaredType;
}
// The real implementation of the next 2 methods uses reflection on concrete types to determine the declaredType hint.
// TypeFromTypeHint and TypeHintPropertyForType are the inverse of each other.
private Type TypeFromTypeHint(JObject jo)
{
if (new JValue("B").Equals(jo["Target"]))
{
return typeof(B);
}
else if (new JValue("A").Equals(jo["Hint"]))
{
return typeof(A);
}
else if (new JValue("C").Equals(jo["Is"]))
{
return typeof(C);
}
else
{
throw new ArgumentException("Type not recognized from JSON");
}
}
private JProperty TypeHintPropertyForType(Type type)
{
if (type == typeof(A))
{
return new JProperty("Hint", "A");
}
else if (type == typeof(B))
{
return new JProperty("Target", "B");
}
else if (type == typeof(C))
{
return new JProperty("Is", "C");
}
else
{
return null;
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (! CanConvert(objectType))
{
throw new InvalidOperationException("Can't convert declaredType " + objectType + "; expected " + _declaredType);
}
// Load JObject from stream. Turns out we're also called for null arrays of our objects,
// so handle a null by returning one.
var jToken = JToken.Load(reader);
if (jToken.Type == JTokenType.Null)
return null;
if (jToken.Type != JTokenType.Object)
{
throw new InvalidOperationException("Json: expected " + _declaredType + "; got " + jToken.Type);
}
JObject jObject = (JObject) jToken;
// Select the declaredType based on TypeHint
Type deserializingType = TypeFromTypeHint(jObject);
var target = Activator.CreateInstance(deserializingType);
serializer.Populate(jObject.CreateReader(), target);
return target;
}
public override bool CanWrite
{
get { return true; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JProperty typeHintProperty = TypeHintPropertyForType(value.GetType());
//BUG: JsonSerializationException : Self referencing loop detected with type 'B'. Path ''.
// Same error occurs whether I use the serializer parameter or a separate serializer.
JObject jo = JObject.FromObject(value, serializer);
if (typeHintProperty != null)
{
jo.AddFirst(typeHintProperty);
}
writer.WriteToken(jo.CreateReader());
}
}
如您所见,
推荐答案
在要转换的同一对象上的转换器中调用JObject.FromObject()
将导致递归循环.通常,解决方案是(a)在转换器内部使用单独的JsonSerializer实例,或(b)手动序列化属性,如James在他的回答中指出的那样.您的情况有点特殊,因为这些解决方案都不适合您:如果您使用不了解转换器的单独的序列化器实例,则子对象将不会应用其提示属性.正如您在评论中提到的那样,完全手动序列化不适用于通用解决方案.
Calling JObject.FromObject()
from within a converter on the same object being converted will result in a recursive loop, as you have seen. Normally the solution is to either (a) use a separate JsonSerializer instance inside the converter, or (b) serialize the properties manually, as James pointed out in his answer. Your case is a little special in that neither of these solutions really work for you: if you use a separate serializer instance that doesn't know about the converter then your child objects will not get their hint properties applied. And serializing completely manually doesn't work for a generalized solution, as you mentioned in your comments.
幸运的是,有一个中间立场.您可以在WriteJson
方法中使用一些反射来获取对象属性,然后从那里委托给JToken.FromObject()
.将对子属性(而不是当前对象)进行递归调用转换器,因此不会麻烦.此解决方案的一个警告:如果碰巧将任何[JsonProperty]
属性应用于此转换器处理的类(在您的示例中为A,B和C),则这些属性将不受尊重.
Fortunately, there is a middle ground. You can use a bit of reflection in your WriteJson
method to get the object properties, then delegate from there to JToken.FromObject()
. The converter will be called recursively as it should for the child properties, but not for the current object, so you don't get into trouble. One caveat with this solution: if you happen to have any [JsonProperty]
attributes applied to the classes handled by this converter (A, B and C in your example), those attributes will not be respected.
这是WriteJson
方法的更新代码:
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JProperty typeHintProperty = TypeHintPropertyForType(value.GetType());
JObject jo = new JObject();
if (typeHintProperty != null)
{
jo.Add(typeHintProperty);
}
foreach (PropertyInfo prop in value.GetType().GetProperties())
{
if (prop.CanRead)
{
object propValue = prop.GetValue(value);
if (propValue != null)
{
jo.Add(prop.Name, JToken.FromObject(propValue, serializer));
}
}
}
jo.WriteTo(writer);
}
提琴: https://dotnetfiddle.net/jQrxb8
这篇关于Json.NET,如何自定义序列化以插入JSON属性的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!