JSON.Net throws StackOverflowException when using [JsonConvert()]
After reading (and testing) Paul Kiar & p.kaneman solution I'd say it seems to be a challenging task to implement WriteJson
. Even though it works for the most cases - there are a few edge cases that are not covered yet.
Examples:
public bool ShouldSerialize*()
methodsnull
values- value types (
struct
) - json converter attributes
- ..
Here is (just) another try:
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
if (ReferenceEquals(value, null)) {
writer.WriteNull();
return;
}
var contract = (JsonObjectContract)serializer
.ContractResolver
.ResolveContract(value.GetType());
writer.WriteStartObject();
foreach (var property in contract.Properties) {
if (property.Ignored) continue;
if (!ShouldSerialize(property, value)) continue;
var property_name = property.PropertyName;
var property_value = property.ValueProvider.GetValue(value);
writer.WritePropertyName(property_name);
if (property.Converter != null && property.Converter.CanWrite) {
property.Converter.WriteJson(writer, property_value, serializer);
} else {
serializer.Serialize(writer, property_value);
}
}
writer.WriteEndObject();
}
private static bool ShouldSerialize(JsonProperty property, object instance) {
return property.ShouldSerialize == null
|| property.ShouldSerialize(instance);
}
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.
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
}
Having done this, you can restore the [JsonConverter(typeof(FJson))]
to your class A
:
[JsonConverter(typeof(FJson))]
public class A
{
}
Demo fiddle #1 here.
A second, simpler workaround for generating a default serialization 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:
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.
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(); }
}
And then use it in FJson.WriteJson()
as follows:
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 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.
Demo fiddle #2 here.
Notes
Both converter versions only handle writing; reading is not implemented.
To solve the equivalent problem during deserialization, see e.g. Json.NET custom serialization with JsonConverter - how to get the "default" behavior.
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.
I didn't like the solution posted above so I worked out how the serializer actually serialized the object and tried to distill it down to the minimum:
public override void WriteJson( JsonWriter writer, object value, JsonSerializer serializer )
{
JsonObjectContract contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract( value.GetType() );
writer.WriteStartObject();
foreach ( var property in contract.Properties )
{
writer.WritePropertyName( property.PropertyName );
writer.WriteValue( property.ValueProvider.GetValue(value));
}
writer.WriteEndObject();
}
No stack overflow problem and no need for a recursive disable flag.