Reputation: 83
Is there a way I can create my own Custom JsonConverter, which modifies the data before writing out the Json, that works with a nested parent-child structure? Whenever I try at the moment, I end up with a self referencing loop error. I've tried searching for solutions, but I can't quite find anything that matches.
I've created a simple solution to demonstrate the problem.
I have the following model and converter
public class NestedModel
{
public int Id { get; set; }
public string Forename { get; set; }
public string Surname { get; set; }
public string Custom { get; set; }
public List<SimpleModel> Children { get; set; }
}
public class NestedModelJsonConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var nestedModel = value as NestedModel;
nestedModel.Custom = "Modified by Json Converter";
//This causes a self referencing loop error
serializer.Serialize(writer, value);
//This resolves the self referencing loop error, but it does not call my custom Json Converter for any of the Children, and instead uses the default serialization
//var jo = JObject.FromObject(value);
//jo.WriteTo(writer);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
// Load JObject from stream
JObject jObject = JObject.Load(reader);
// Create target object based on JObject
NestedModel target = new NestedModel();
// Populate the object properties
StringWriter writer = new StringWriter();
serializer.Serialize(writer, jObject);
using (JsonTextReader newReader = new JsonTextReader(new StringReader(writer.ToString())))
{
newReader.Culture = reader.Culture;
newReader.DateParseHandling = reader.DateParseHandling;
newReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
newReader.FloatParseHandling = reader.FloatParseHandling;
serializer.Populate(newReader, target);
}
return target;
}
public override bool CanRead { get { return true; } }
public override bool CanWrite { get { return true; } }
public override bool CanConvert(Type objectType) { return typeof(NestedModel).IsAssignableFrom(objectType); }
}
and the test code to use it
string sourceJson = @"{
""Id"": 1,
""Forename"": ""John"",
""Surname"": ""Smith"",
""Children"":
[
{
""Id"": 2,
""Forename"": ""Joe"",
""Surname"": ""Bloggs"",
""Children"": null
}
]
}";
var settings = new JsonSerializerSettings()
{
Converters = new List<JsonConverter>()
{
new NestedModelJsonConverter()
},
Formatting = Formatting.Indented
};
var nestedModel = JsonConvert.DeserializeObject<NestedModel>(sourceJson, settings);
string outputJson = JsonConvert.SerializeObject(nestedModel, settings);
However when it tries to Write the Json, it gives a self referencing loop error, presumably when it tries to process the List of Children. I can prevent that error by using a JObject for the conversion, but then that prevents my custom converter from being used for the child elements. I want to have my custom WriteJson method fire for each level of that structure so that I can modify some data before writing it.
Is there a way to do this and get around the self referencing loop error?
Upvotes: 1
Views: 1946
Reputation: 83
After some more work, I found a way to do it, so I thought I'd post it for anyone else having a similar issue. I had to ensure that when Serializing the NestedModel, I individually serialized each property using reflection (then if that property was a list of NestedModel, it called my serializer again for each one).
Here's the model / custom converter
public class NestedModel
{
public int Id { get; set; }
public string Forename { get; set; }
public string Surname { get; set; }
public string Custom { get; set; }
public List<NestedModel> Children { get; set; }
}
public class NestedModelJsonConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var nestedModel = value as NestedModel;
nestedModel.Custom = "Modified by Json Converter";
JObject jo = new JObject();
Type type = nestedModel.GetType();
foreach (PropertyInfo prop in type.GetProperties())
{
if (prop.CanRead)
{
object propVal = prop.GetValue(nestedModel, null);
if (propVal != null)
{
jo.Add(prop.Name, JToken.FromObject(propVal, serializer));
}
}
}
jo.WriteTo(writer);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
// Load JObject from stream
JObject jObject = JObject.Load(reader);
// Create target object based on JObject
NestedModel target = new NestedModel();
// Populate the object properties
StringWriter writer = new StringWriter();
serializer.Serialize(writer, jObject);
using (JsonTextReader newReader = new JsonTextReader(new StringReader(writer.ToString())))
{
newReader.Culture = reader.Culture;
newReader.DateParseHandling = reader.DateParseHandling;
newReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
newReader.FloatParseHandling = reader.FloatParseHandling;
serializer.Populate(newReader, target);
}
return target;
}
public override bool CanRead { get { return true; } }
public override bool CanWrite { get { return true; } }
public override bool CanConvert(Type objectType)
{
return typeof(NestedModel).IsAssignableFrom(objectType);
}
}
and the test code
string sourceJson = @"{
""Id"": 1,
""Forename"": ""John"",
""Surname"": ""Smith"",
""Children"":
[
{
""Id"": 2,
""Forename"": ""Joe"",
""Surname"": ""Bloggs"",
""Children"": null
}
]
}";
var settings = new JsonSerializerSettings()
{
Converters = new List<JsonConverter>()
{
new NestedModelJsonConverter()
},
Formatting = Formatting.Indented,
};
var nestedModel = JsonConvert.DeserializeObject<NestedModel>(sourceJson, settings);
string outputJson = JsonConvert.SerializeObject(nestedModel, settings);
the resulting Json created is then
{
"Id": 1,
"Forename": "John",
"Surname": "Smith",
"Custom": "Modified by Json Converter",
"Children": [
{
"Id": 2,
"Forename": "Joe",
"Surname": "Bloggs",
"Custom": "Modified by Json Converter"
}
]
}
Upvotes: 1