JSON.Net 在使用 [JsonConvert()] 时抛出 StackOverflowException [英] JSON.Net throws StackOverflowException when using [JsonConvert()]
问题描述
我编写了这个简单的代码来将类序列化为扁平化,但是当我使用 [JsonConverter(typeof(FJson))]
注释时,它会抛出一个 StackOverflowException.如果我手动调用 SerializeObject
,它工作正常.
I wrote this simple code to Serialize classes as flatten, but when I use [JsonConverter(typeof(FJson))]
annotation, it throws a StackOverflowException. If I call the SerializeObject
manually, it works fine.
如何在 Annotation 模式下使用 JsonConvert:
How can I use JsonConvert in Annotation mode:
class Program
{
static void Main(string[] args)
{
A a = new A();
a.id = 1;
a.b.name = "value";
string json = null;
// json = JsonConvert.SerializeObject(a, new FJson()); without [JsonConverter(typeof(FJson))] annotation workd fine
// json = JsonConvert.SerializeObject(a); StackOverflowException
Console.WriteLine(json);
Console.ReadLine();
}
}
//[JsonConverter(typeof(FJson))] StackOverflowException
public class A
{
public A()
{
this.b = new B();
}
public int id { get; set; }
public string name { get; set; }
public B b { get; set; }
}
public class B
{
public string name { get; set; }
}
public class FJson : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JToken t = JToken.FromObject(value);
if (t.Type != JTokenType.Object)
{
t.WriteTo(writer);
return;
}
JObject o = (JObject)t;
writer.WriteStartObject();
WriteJson(writer, o);
writer.WriteEndObject();
}
private void WriteJson(JsonWriter writer, JObject value)
{
foreach (var p in value.Properties())
{
if (p.Value is JObject)
WriteJson(writer, (JObject)p.Value);
else
p.WriteTo(writer);
}
}
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override bool CanConvert(Type objectType)
{
return true; // works for any type
}
}
推荐答案
Json.NET 对调用 JToken.FromObject
以生成默认"序列化,然后修改结果 JToken
用于输出 - 正是因为 StackOverflowException
由于对 JsonConverter.WriteJson()
的递归调用而导致您观察到的发生.
Json.NET does not have convenient support for converters that call JToken.FromObject
to generate a "default" serialization and then modify the resulting JToken
for output - precisely because the StackOverflowException
due to recursive calls to JsonConverter.WriteJson()
that you have observed will occur.
一种解决方法是使用线程静态布尔值在递归调用中临时禁用转换器.使用线程静态是因为,在某些情况下,包括 asp.net-web-api,JSON 转换器的实例将在线程之间共享.在这种情况下,通过实例属性禁用转换器将不是线程安全的.
One workaround is to temporarily disable the converter in recursive calls using a thread static Boolean. A thread static is used because, in some situations including asp.net-web-api, instances of JSON converters will be shared between threads. In such situations disabling the converter via an instance property will not be thread-safe.
public class FJson : JsonConverter
{
[ThreadStatic]
static bool disabled;
// Disables the converter in a thread-safe manner.
bool Disabled { get { return disabled; } set { disabled = value; } }
public override bool CanWrite { get { return !Disabled; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JToken t;
using (new PushValue<bool>(true, () => Disabled, (canWrite) => Disabled = canWrite))
{
t = JToken.FromObject(value, serializer);
}
if (t.Type != JTokenType.Object)
{
t.WriteTo(writer);
return;
}
JObject o = (JObject)t;
writer.WriteStartObject();
WriteJson(writer, o);
writer.WriteEndObject();
}
private void WriteJson(JsonWriter writer, JObject value)
{
foreach (var p in value.Properties())
{
if (p.Value is JObject)
WriteJson(writer, (JObject)p.Value);
else
p.WriteTo(writer);
}
}
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override bool CanConvert(Type objectType)
{
return true; // works for any type
}
}
public struct PushValue<T> : IDisposable
{
Action<T> setValue;
T oldValue;
public PushValue(T value, Func<T> getValue, Action<T> setValue)
{
if (getValue == null || setValue == null)
throw new ArgumentNullException();
this.setValue = setValue;
this.oldValue = getValue();
setValue(value);
}
#region IDisposable Members
// By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class.
public void Dispose()
{
if (setValue != null)
setValue(oldValue);
}
#endregion
}
完成此操作后,您可以将 [JsonConverter(typeof(FJson))]
恢复到您的类 A
:
Having done this, you can restore the [JsonConverter(typeof(FJson))]
to your class A
:
[JsonConverter(typeof(FJson))]
public class A
{
}
演示小提琴 #1 此处.
第二种更简单的解决方法,用于为应用了 JsonConverter
的类型生成默认的 JToken
表示,利用了转换器应用于成员取代应用于类型或设置中的转换器.来自 文档:
A second, simpler workaround for generating a default JToken
representation for a type with a JsonConverter
applied takes advantage fact that a converter applied to a member supersedes converters applied to the type, or in settings. From the docs:
使用 JsonConverter 的优先级是成员上的属性定义的 JsonConverter,然后是类上的属性定义的 JsonConverter,最后是传递给 JsonSerializer 的所有转换器.
The priority of which JsonConverter is used is the JsonConverter defined by attribute on a member, then the JsonConverter defined by an attribute on a class, and finally any converters passed to the JsonSerializer.
因此,可以通过将其嵌套在 DTO 有一个成员,其值是您的类型的一个实例,并且应用了一个虚拟转换器,它只会回退到读取和写入的默认序列化.
Thus it is possible to generate a default serialization for your type by nesting it inside a DTO with a single member whose value is an instance of your type and has a dummy converter applied which does nothing but fall back to to default serialization for both reading and writing.
以下扩展方法和转换器可以完成这项工作:
The following extension method and converter do the job:
public static partial class JsonExtensions
{
public static JToken DefaultFromObject(this JsonSerializer serializer, object value)
{
if (value == null)
return JValue.CreateNull();
var dto = Activator.CreateInstance(typeof(DefaultSerializationDTO<>).MakeGenericType(value.GetType()), value);
var root = JObject.FromObject(dto, serializer);
return root["Value"].RemoveFromLowestPossibleParent() ?? JValue.CreateNull();
}
public static object DefaultToObject(this JToken token, Type type, JsonSerializer serializer = null)
{
var oldParent = token.Parent;
var dtoToken = new JObject(new JProperty("Value", token));
var dtoType = typeof(DefaultSerializationDTO<>).MakeGenericType(type);
var dto = (IHasValue)(serializer ?? JsonSerializer.CreateDefault()).Deserialize(dtoToken.CreateReader(), dtoType);
if (oldParent == null)
token.RemoveFromLowestPossibleParent();
return dto == null ? null : dto.GetValue();
}
public static JToken RemoveFromLowestPossibleParent(this JToken node)
{
if (node == null)
return null;
// If the parent is a JProperty, remove that instead of the token itself.
var contained = node.Parent is JProperty ? node.Parent : node;
contained.Remove();
// Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
if (contained is JProperty)
((JProperty)node.Parent).Value = null;
return node;
}
interface IHasValue
{
object GetValue();
}
[JsonObject(NamingStrategyType = typeof(DefaultNamingStrategy), IsReference = false)]
class DefaultSerializationDTO<T> : IHasValue
{
public DefaultSerializationDTO(T value) { this.Value = value; }
public DefaultSerializationDTO() { }
[JsonConverter(typeof(NoConverter)), JsonProperty(ReferenceLoopHandling = ReferenceLoopHandling.Serialize)]
public T Value { get; set; }
object IHasValue.GetValue() { return Value; }
}
}
public class NoConverter : JsonConverter
{
// NoConverter taken from this answer https://stackoverflow.com/a/39739105/3744182
// To https://stackoverflow.com/questions/39738714/selectively-use-default-json-converter
// By https://stackoverflow.com/users/3744182/dbc
public override bool CanConvert(Type objectType) { throw new NotImplementedException(); /* This converter should only be applied via attributes */ }
public override bool CanRead { get { return false; } }
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new NotImplementedException(); }
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); }
}
然后在FJson.WriteJson()
中使用如下:
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JToken t = serializer.DefaultFromObject(value);
// Remainder as before
if (t.Type != JTokenType.Object)
{
t.WriteTo(writer);
return;
}
JObject o = (JObject)t;
writer.WriteStartObject();
WriteJson(writer, o);
writer.WriteEndObject();
}
这种方法的优点和缺点是:
The advantages and disadvantages of this approach are that:
它不依赖于递归禁用转换器,因此可以与递归数据模型一起正常工作.
It doesn't rely on recursively disabling the converter, and so works correctly with recursive data models.
它不需要重新实现从对象的属性序列化对象的整个逻辑.
It doesn't require re-implementing the entire logic of serializing an object from its properties.
它序列化为中间的 JToken
表示并从中反序列化.当尝试将默认序列化直接传入和传出传入的 JsonReader
或 JsonWriter
时,不适合使用.
It serializes to and deserializes from an intermediate JToken
representation. It is not appropriate for use when attempt to stream a default serialization directly to and from a the incoming JsonReader
or JsonWriter
.
演示小提琴 #2 此处.
注意事项
两个转换器版本都只处理写入;未实现读取.
Both converter versions only handle writing; reading is not implemented.
要解决反序列化过程中的等效问题,请参见例如使用 JsonConverter 进行 Json.NET 自定义序列化 - 如何获得默认"行为.
To solve the equivalent problem during deserialization, see e.g. Json.NET custom serialization with JsonConverter - how to get the "default" behavior.
您编写的转换器创建了具有重复名称的 JSON:
Your converter as written creates JSON with duplicated names:
{
"id": 1,
"name": null,
"name": "value"
}
这虽然不是严格非法的,但通常被认为是不良做法,因此应该避免.>
This, while not strictly illegal, is generally considered to be bad practice and so should probably be avoided.
这篇关于JSON.Net 在使用 [JsonConvert()] 时抛出 StackOverflowException的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!