Json.net deserialize complex object with concurrent collection in composition - c#

I have a class like this:
public class ComplexClass
{
public ConcurrentBag<SimpleClass> _simpleClassObjects;
}
When i serialize this class, it works. But when i try to deserialize
public static ComplexClass LoadComplexClass()
{
ComplexClass persistedComplexClass;
using (var stream = new StreamReader(File.Open(jsonFilePath, FileMode.Open)))
{
persistedComplexClass = (ComplexClass) JsonSerializer.Create().Deserialize(stream, typeof(ComplexClass));
}
return persistedComplexClass;
}
it throws the exception:
An unhandled exception of type 'System.InvalidCastException' occurred in Newtonsoft.Json.dll
Additional information: Unable to cast object of type 'System.Collections.Concurrent.ConcurrentBag`1[LabML.Model.Point]' to type 'System.Collections.Generic.ICollection`1[LabML.Model.Point]'.
This root cause of this exception is that the ConcurrentBag<T> doesn't implements generic ICollection<T>, only non-generic ICollection.
How to resolve this using Json.Net? (I've searched a while for this, but only what i found is about mapping an ICollection<T> to ConcurrentCollection not in Complex Classes.

Update
As of Release 10.0.3, Json.NET claims to correctly serialize ConcurrentBag<T>. According to the release notes:
Fix - Fixed serializing ConcurrentStack/Queue/Bag
Original Answer
As you surmise, the problem is that ConcurrentBag<T> implements ICollection and IEnumerable<T> but not ICollection<T> so Json.NET does not know how to add items to it and treats it as a read-only collection. While ConcurrentBag<T> does have a parameterized constructor taking an input collection, Json.NET will not use that constructor because it also, internally, has [OnSerializing] and [OnDeserialized] callbacks. Json.NET will not use a parameterized constructor when these callbacks are present, instead throwing an exception
Cannot call OnSerializing on an array or readonly list, or list created from a non-default constructor: System.Collections.Concurrent.ConcurrentBag`1[]
Thus it is necessary to create a custom JsonConverter for ConcurrentBag<T>:
public class ConcurrentBagConverter : ConcurrentBagConverterBase
{
public override bool CanConvert(Type objectType)
{
return objectType.GetConcurrentBagItemType() != null;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
try
{
var itemType = objectType.GetConcurrentBagItemType();
var method = GetType().GetMethod("ReadJsonGeneric", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy);
var genericMethod = method.MakeGenericMethod(new[] { objectType, itemType });
return genericMethod.Invoke(this, new object[] { reader, objectType, itemType, existingValue, serializer });
}
catch (TargetInvocationException ex)
{
// Wrap the TargetInvocationException in a JsonSerializationException
throw new JsonSerializationException("Failed to deserialize " + objectType, ex);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var objectType = value.GetType();
try
{
var itemType = objectType.GetConcurrentBagItemType();
var method = GetType().GetMethod("WriteJsonGeneric", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy);
var genericMethod = method.MakeGenericMethod(new[] { objectType, itemType });
genericMethod.Invoke(this, new object[] { writer, value, serializer });
}
catch (TargetInvocationException ex)
{
// Wrap the TargetInvocationException in a JsonSerializationException
throw new JsonSerializationException("Failed to serialize " + objectType, ex);
}
}
}
public class ConcurrentBagConverter<TItem> : ConcurrentBagConverterBase
{
public override bool CanConvert(Type objectType)
{
return typeof(ConcurrentBagConverter<TItem>).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return ReadJsonGeneric<ConcurrentBag<TItem>, TItem>(reader, objectType, typeof(TItem), existingValue, serializer);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
WriteJsonGeneric<ConcurrentBag<TItem>, TItem>(writer, value, serializer);
}
}
// https://stackoverflow.com/questions/42836648/json-net-deserialize-complex-object-with-concurrent-collection-in-composition
public abstract class ConcurrentBagConverterBase : JsonConverter
{
protected TConcurrentBag ReadJsonGeneric<TConcurrentBag, TItem>(JsonReader reader, Type collectionType, Type itemType, object existingValue, JsonSerializer serializer)
where TConcurrentBag : ConcurrentBag<TItem>
{
if (reader.TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.StartArray)
throw new JsonSerializationException(string.Format("Expected {0}, encountered {1} at path {2}", JsonToken.StartArray, reader.TokenType, reader.Path));
var collection = existingValue as TConcurrentBag ?? (TConcurrentBag)serializer.ContractResolver.ResolveContract(collectionType).DefaultCreator();
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.Comment:
break;
case JsonToken.EndArray:
return collection;
default:
collection.Add((TItem)serializer.Deserialize(reader, itemType));
break;
}
}
// Should not come here.
throw new JsonSerializationException("Unclosed array at path: " + reader.Path);
}
protected void WriteJsonGeneric<TConcurrentBag, TItem>(JsonWriter writer, object value, JsonSerializer serializer)
where TConcurrentBag : ConcurrentBag<TItem>
{
// Snapshot the bag as an array and serialize the array.
var array = ((TConcurrentBag)value).ToArray();
serializer.Serialize(writer, array);
}
}
internal static class TypeExtensions
{
public static Type GetConcurrentBagItemType(this Type objectType)
{
while (objectType != null)
{
if (objectType.IsGenericType
&& objectType.GetGenericTypeDefinition() == typeof(ConcurrentBag<>))
{
return objectType.GetGenericArguments()[0];
}
objectType = objectType.BaseType;
}
return null;
}
}
public class ConcurrentBagContractResolver : DefaultContractResolver
{
protected override JsonArrayContract CreateArrayContract(Type objectType)
{
var contract = base.CreateArrayContract(objectType);
var concurrentItemType = objectType.GetConcurrentBagItemType();
if (concurrentItemType != null)
{
if (contract.Converter == null)
contract.Converter = (JsonConverter)Activator.CreateInstance(typeof(ConcurrentBagConverter<>).MakeGenericType(new[] { concurrentItemType }));
}
return contract;
}
}
Then, apply the generic version to your specific field as follows:
public class ComplexClass
{
[JsonConverter(typeof(ConcurrentBagConverter<SimpleClass>))]
public ConcurrentBag<SimpleClass> _simpleClassObjects;
}
Or, apply a universal version globally for all ConcurrentBag<T> for any T using the following settings:
var settings = new JsonSerializerSettings
{
Converters = { new ConcurrentBagConverter() },
};
Alternatively a custom contract resolver could be used, which might have slightly better performance than using the universal converter:
var settings = new JsonSerializerSettings
{
ContractResolver = new ConcurrentBagContractResolver(),
};
Example fiddle.
That being said, the above only work if the ConcurrentBag<T> property or field is read/write. If the member is read-only then I have found that Json.NET 9.0.1 will skip deserialization even if a converter is present because it infers that the collection member and contents are both read-only. (This may be a bug in JsonSerializerInternalReader.CalculatePropertyDetails().)
As a workaround, you could make the property be privately settable, and mark it with [JsonProperty]:
public class ComplexClass
{
ConcurrentBag<SimpleClass> m_simpleClassObjects = new ConcurrentBag<SimpleClass>();
[JsonConverter(typeof(ConcurrentBagConverter<SimpleClass>))]
[JsonProperty]
public ConcurrentBag<SimpleClass> _simpleClassObjects { get { return m_simpleClassObjects; } private set { m_simpleClassObjects = value; } }
}
Or use a surrogate array property, thereby eliminating the need for any sort of converter:
public class ComplexClass
{
readonly ConcurrentBag<SimpleClass> m_simpleClassObjects = new ConcurrentBag<SimpleClass>();
[JsonIgnore]
public ConcurrentBag<SimpleClass> _simpleClassObjects { get { return m_simpleClassObjects; } }
[JsonProperty("_simpleClassObjects")]
SimpleClass[] _simpleClassObjectsArray
{
get
{
return _simpleClassObjects.ToArray();
}
set
{
if (value == null)
return;
foreach (var item in value)
_simpleClassObjects.Add(item);
}
}
}

Related

Can I set my own default List<T> converter in JSON.net (without attributes)

Service which I work with uses strange serialization. When array is empty it looks like this:
"SomeArr":[]
But when 'SomeArr' has items it looks like this:
"SomeArr":
{
"item1": { "prop1":"value1" },
"item2": { "prop1":"value1" }
...
}
So it's not even array now but JObject with properties instead of array enumerators
I have this converter that must be applied to all properties with List type
public class ArrayObjectConverter<T> : JsonConverter<List<T>>
{
public override void WriteJson(JsonWriter writer, List<T>? value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override List<T>? ReadJson(JsonReader reader, Type objectType, List<T>? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
List<T> result = new();
if (reader.TokenType == JsonToken.StartArray)
{
var jArray = JArray.Load(reader);
//'LINQ Select' because sometimes arrays are normal
//So if I set this converter as default we select objects from this array
return jArray.Select(jt => jt.ToObject<T>()!).ToList();
}
else
{
var jObject = JObject.Load(reader);
foreach (var kvp in jObject)
{
var obj = kvp.Value!.ToObject<T>()!;
result.Add(obj);
}
return result;
}
}
}
So how I can set this converter as default (e.g. in serializer.settings). The problem is this converter is generic type and I can't set in settings without generic argument.
Of course I can put [JsonConverter(typeof(ArrayObjectConverter<T>))] attribute for every collection. But my json classes already have a lot of boilerplate. Any suggestions?
P.S. The solution should be as optimized as possible because the speed of deserialization is very important.
You can take advantage of the fact that List<T> implements the non-generic interface IList to create a non-generic JsonConverter for all List<T> types:
public class ArrayObjectConverter : JsonConverter
{
public override bool CanConvert(Type t) => t.GetListItemType() != null;
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => throw new NotImplementedException();
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
System.Diagnostics.Debug.Assert(objectType.GetListItemType() != null);
if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
return null;
IList value = existingValue as IList ?? (IList)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator!();
if (reader.TokenType == JsonToken.StartArray)
{
serializer.Populate(reader, value);
}
else if (reader.TokenType == JsonToken.StartObject)
{
var itemType = objectType.GetListItemType().ThrowOnNull();
while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndObject)
{
// Eat the property name
reader.AssertTokenType(JsonToken.PropertyName).ReadToContentAndAssert();
// Deserialize the property value and add it to the list.
value.Add(serializer.Deserialize(reader, itemType));
}
}
else
{
throw new JsonSerializationException(string.Format("Unknown token type {0}", reader.TokenType));
}
return value;
}
}
public static partial class JsonExtensions
{
public static JsonReader AssertTokenType(this JsonReader reader, JsonToken tokenType) =>
reader.TokenType == tokenType ? reader : throw new JsonSerializationException(string.Format("Unexpected token {0}, expected {1}", reader.TokenType, tokenType));
public static JsonReader ReadToContentAndAssert(this JsonReader reader) =>
reader.ReadAndAssert().MoveToContentAndAssert();
public static JsonReader MoveToContentAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (reader.TokenType == JsonToken.None) // Skip past beginning of stream.
reader.ReadAndAssert();
while (reader.TokenType == JsonToken.Comment) // Skip past comments.
reader.ReadAndAssert();
return reader;
}
public static JsonReader ReadAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (!reader.Read())
throw new JsonReaderException("Unexpected end of JSON stream.");
return reader;
}
public static Type? GetListItemType(this Type type)
{
// Quick reject for performance
if (type.IsPrimitive || type.IsArray || type == typeof(string))
return null;
while (type != null)
{
if (type.IsGenericType)
{
var genType = type.GetGenericTypeDefinition();
if (genType == typeof(List<>))
return type.GetGenericArguments()[0];
}
type = type.BaseType!;
}
return null;
}
}
public static partial class ObjectExtensions
{
public static T ThrowOnNull<T>(this T? value) where T : class => value ?? throw new ArgumentNullException(nameof(value));
}
Notes:
Your question mentions The solution should be as optimized as possible so the converter deserializes directly from the JsonReader without needing to pre-load anything into intermediate JArray or JObject instances.
The converter should work for subclasses of List<T> as well.
If you need to support types that implement ICollection<T> types but do not also implement the non-generic IList interface (such as HashSet<T>), you will need to use reflection to invoke a generic method from the non-generic ReadJson() e.g. as shown in this answer to Newtonsoft Json Deserialize Dictionary as Key/Value list from DataContractJsonSerializer.
Demo fiddle here.
In the similar cases I prefer to create a JsonConstructor for the class
MyClass someArr=JsonConvert.DeserializeObject<MyClass>(json);
public class MyClass
{
public Dictionary<string, object> SomeArr {get; set;}
[JsonConstructor]
public MyClass(JToken SomeArr)
{
if(SomeArr.Type.ToString() != "Array")
this.SomeArr=SomeArr.ToObject<Dictionary<string,object>>();
}
}
You don't need to include all class properties in the constructor. Only the properties that need a special treatment.

How to deserialize objects with key property synchronization into an existing collection?

I'm trying to use Json.NET to deserialize JSON data into an existing hierarchy using JsonConvert.PopulateObject. Everything is fine except for child collections.
I would like to synchronize the target collection items with the deserialized ones, so that target items would be updated with source objects with matching keys, non-existing target objects would be added, and non-existing source objects would be removed.
How and where could I customize the deserialization logic to achieve this behavior?
Building on the answer to Json.Net PopulateObject - update list elements based on ID, here's a pair of converters that will synchronize an existing list with a list deserialized from JSON, adding, removing or populating list items based on a key property:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class JsonMergeKeyAttribute : System.Attribute
{
}
public abstract class KeyedListSynchronizingConverterBase : JsonConverter
{
protected static bool CanConvert(IContractResolver contractResolver, Type objectType, out Type elementType, out JsonProperty keyProperty)
{
if (objectType.IsArray)
{
// Not implemented for arrays, since they cannot be resized.
elementType = null;
keyProperty = null;
return false;
}
var elementTypes = objectType.GetIListItemTypes().ToList();
if (elementTypes.Count != 1)
{
elementType = null;
keyProperty = null;
return false;
}
elementType = elementTypes[0];
var contract = contractResolver.ResolveContract(elementType) as JsonObjectContract;
if (contract == null)
{
keyProperty = null;
return false;
}
keyProperty = contract.Properties.Where(p => p.AttributeProvider.GetAttributes(typeof(JsonMergeKeyAttribute), true).Count > 0).SingleOrDefault();
return keyProperty != null;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var contractResolver = serializer.ContractResolver;
Type elementType;
JsonProperty keyProperty;
if (!CanConvert(contractResolver, objectType, out elementType, out keyProperty))
throw new JsonSerializationException(string.Format("Invalid input type {0}", objectType));
if (elementType.IsValueType)
throw new NotImplementedException("Not implemented for value types");
if (reader.TokenType == JsonToken.Null)
return null;
var method = typeof(KeyedListSynchronizingConverterBase).GetMethod("ReadJsonGeneric", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
var genericMethod = method.MakeGenericMethod(new[] { elementType });
try
{
return genericMethod.Invoke(this, new object[] { reader, objectType, existingValue, serializer, keyProperty });
}
catch (TargetInvocationException ex)
{
// Wrap the TargetInvocationException in a JsonSerializationException
throw new JsonSerializationException("ReadJsonGeneric<T> error", ex);
}
}
object ReadJsonGeneric<T>(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer, JsonProperty keyProperty) where T : class
{
var list = existingValue as IList<T>;
if (list == null || list.Count == 0)
{
var contractResolver = serializer.ContractResolver;
list = list ?? (IList<T>)contractResolver.ResolveContract(objectType).DefaultCreator();
serializer.Populate(reader, list);
}
else
{
var jArray = JArray.Load(reader);
var lookup = Enumerable.Range(0, list.Count)
.Where(i => list[i] != null)
.ToLookup(i => keyProperty.ValueProvider.GetValue(list[i]), i => KeyValuePair.Create(i, list[i]), EqualityComparer<object>.Default);
var done = new HashSet<int>(); // In case there are duplicate keys, pair them in order.
for (int i = 0, count = jArray.Count; i < count; i++)
{
T item;
if (jArray[i].Type == JTokenType.Null)
item = null;
else
{
var key = jArray[i][keyProperty.PropertyName].ToObject(keyProperty.PropertyType, serializer);
var pair = lookup[key].Where(p => !done.Contains(p.Key)).FirstOrDefault();
item = pair.Value;
if (item == null)
{
item = jArray[i].ToObject<T>(serializer);
}
else
{
using (var subReader = jArray[i].CreateReader())
serializer.Populate(subReader, item);
}
done.Add(pair.Key);
}
if (i < list.Count)
list[i] = item;
else
list.Add(item);
}
while (list.Count > jArray.Count)
list.RemoveAt(list.Count - 1);
}
return list;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
public class KeyedListSynchronizingConverter : KeyedListSynchronizingConverterBase
{
readonly IContractResolver contractResolver;
public KeyedListSynchronizingConverter(IContractResolver contractResolver)
{
if (contractResolver == null)
throw new ArgumentNullException("contractResolver");
this.contractResolver = contractResolver;
}
public override bool CanConvert(Type objectType)
{
Type elementType;
JsonProperty keyProperty;
return CanConvert(contractResolver, objectType, out elementType, out keyProperty);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (contractResolver != serializer.ContractResolver)
throw new InvalidOperationException("Inconsistent contract resolvers");
return base.ReadJson(reader, objectType, existingValue, serializer);
}
}
public class KeyedListPropertySynchronizingConverter : KeyedListSynchronizingConverterBase
{
public override bool CanConvert(Type objectType)
{
throw new NotImplementedException("This converter is intended to be applied to a specific property, rather than globally");
}
}
Then apply the key attribute to your collection items as follows, to indicate the key to use for merging:
public class Item
{
[JsonMergeKey]
public Guid Id { get; set; }
public string Value { get; set; }
}
Then it can be used as follows globally:
var contractResolver = JsonSerializer.CreateDefault().ContractResolver;
var settings = new JsonSerializerSettings { ContractResolver = contractResolver, Converters = new [] { new KeyedListSynchronizingConverter(contractResolver) } };
JsonConvert.PopulateObject(newJson, rootObject, settings);
(Note that the root object should not be the IList<T> to be synchronized, since Json.NET doesn't call a converter for the root object when doing a Populate().)
Alternatively, you can apply it to a specific IList<T> property as follows:
public class RootObject
{
[JsonConverter(typeof(KeyedListPropertySynchronizingConverter))]
public ObservableCollection<Item> Items { get; set; } // Can be any type of collection implementing IList<T>
}

Using custom JsonConverter and TypeNameHandling in Json.net

I have a class with an interface-typed property like:
public class Foo
{
public IBar Bar { get; set; }
}
I also have multiple concrete implementations of the IBar interface that can be set at runtime. Some of these concrete classes require a custom JsonConverter for serialization & deserialization.
Utilizing the TypeNameHandling.Auto option the non-convertor requiring IBar classes can be serialized and deserialized perfectly. The custom-serialized classes on the other hand have no $type name output and while they are serialized as expected, they cannot be deserialized to their concrete type.
I attempted to write-out the $type name metadata myself within the custom JsonConverter; however, on deserialization the converter is then being bypassed entirely.
Is there a workaround or proper way of handling such a situation?
I solved the similar problem and I found a solution. It's not very elegant and I think there should be a better way, but at least it works. So my idea was to have JsonConverter per each type that implements IBar and one converter for IBar itself.
So let's start from models:
public interface IBar { }
public class BarA : IBar { }
public class Foo
{
public IBar Bar { get; set; }
}
Now let's create converter for IBar. It will be used only when deserializing JSON. It will try to read $type variable and call converter for implementing type:
public class BarConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotSupportedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jObj = JObject.Load(reader);
var type = jObj.Value<string>("$type");
if (type == GetTypeString<BarA>())
{
return new BarAJsonConverter().ReadJson(reader, objectType, jObj, serializer);
}
// Other implementations if IBar
throw new NotSupportedException();
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof (IBar);
}
public override bool CanWrite
{
get { return false; }
}
private string GetTypeString<T>()
{
var typeOfT = typeof (T);
return string.Format("{0}, {1}", typeOfT.FullName, typeOfT.Assembly.GetName().Name);
}
}
And this is converter for BarA class:
public class BarAJsonConverter : BarBaseJsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// '$type' property will be added because used serializer has TypeNameHandling = TypeNameHandling.Objects
GetSerializer().Serialize(writer, value);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var existingJObj = existingValue as JObject;
if (existingJObj != null)
{
return existingJObj.ToObject<BarA>(GetSerializer());
}
throw new NotImplementedException();
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(BarA);
}
}
You may notice that it's inherited from BarBaseJsonConverter class, not JsonConverter. And also we do not use serializer parameter in WriteJson and ReadJson methods. There is a problem with using serializer parameter inside custom converters. You can read more here. We need to create new instance of JsonSerializer and base class is a good candidate for that:
public abstract class BarBaseJsonConverter : JsonConverter
{
public JsonSerializer GetSerializer()
{
var serializerSettings = JsonHelper.DefaultSerializerSettings;
serializerSettings.TypeNameHandling = TypeNameHandling.Objects;
var converters = serializerSettings.Converters != null
? serializerSettings.Converters.ToList()
: new List<JsonConverter>();
var thisConverter = converters.FirstOrDefault(x => x.GetType() == GetType());
if (thisConverter != null)
{
converters.Remove(thisConverter);
}
serializerSettings.Converters = converters;
return JsonSerializer.Create(serializerSettings);
}
}
JsonHelper is just a class to create JsonSerializerSettings:
public static class JsonHelper
{
public static JsonSerializerSettings DefaultSerializerSettings
{
get
{
return new JsonSerializerSettings
{
Converters = new JsonConverter[] { new BarConverter(), new BarAJsonConverter() }
};
}
}
}
Now it will work and you still can use your custom converters for both serialization and deserialization:
var obj = new Foo { Bar = new BarA() };
var json = JsonConvert.SerializeObject(obj, JsonHelper.DefaultSerializerSettings);
var dObj = JsonConvert.DeserializeObject<Foo>(json, JsonHelper.DefaultSerializerSettings);
Using information from Alesandr Ivanov's answer above, I created a generic WrappedJsonConverter<T> class that wraps (and unwraps) concrete classes requiring a converter using a $wrappedType metadata property that follows the same type name serialization as the standard $type.
The WrappedJsonConverter<T> is added as a converter to the Interface (ie. IBar), but otherwise this wrapper is completely transparent to classes that do not require a converter and also requires no changes to the wrapped converters.
I used a slightly different hack to get around the converter/serializer looping (static fields), but it does not require any knowledge of the serializer settings being used, and allows for the IBar object graph to have child IBar properties.
For wrapped objects the Json looks like:
"IBarProperty" : {
"$wrappedType" : "Namespace.ConcreteBar, Namespace",
"$wrappedValue" : {
"ConvertedID" : 90,
"ConvertedPropID" : 70
...
}
}
The full gist can be found here.
public class WrappedJsonConverter<T> : JsonConverter<T> where T : class
{
[ThreadStatic]
private static bool _canWrite = true;
[ThreadStatic]
private static bool _canRead = true;
public override bool CanWrite
{
get
{
if (_canWrite)
return true;
_canWrite = true;
return false;
}
}
public override bool CanRead
{
get
{
if (_canRead)
return true;
_canRead = true;
return false;
}
}
public override T ReadJson(JsonReader reader, T existingValue, JsonSerializer serializer)
{
var jsonObject = JObject.Load(reader);
JToken token;
T value;
if (!jsonObject.TryGetValue("$wrappedType", out token))
{
//The static _canRead is a terrible hack to get around the serialization loop...
_canRead = false;
value = jsonObject.ToObject<T>(serializer);
_canRead = true;
return value;
}
var typeName = jsonObject.GetValue("$wrappedType").Value<string>();
var type = JsonExtensions.GetTypeFromJsonTypeName(typeName, serializer.Binder);
var converter = serializer.Converters.FirstOrDefault(c => c.CanConvert(type) && c.CanRead);
var wrappedObjectReader = jsonObject.GetValue("$wrappedValue").CreateReader();
wrappedObjectReader.Read();
if (converter == null)
{
_canRead = false;
value = (T)serializer.Deserialize(wrappedObjectReader, type);
_canRead = true;
}
else
{
value = (T)converter.ReadJson(wrappedObjectReader, type, existingValue, serializer);
}
return value;
}
public override void WriteJson(JsonWriter writer, T value, JsonSerializer serializer)
{
var type = value.GetType();
var converter = serializer.Converters.FirstOrDefault(c => c.CanConvert(type) && c.CanWrite);
if (converter == null)
{
//This is a terrible hack to get around the serialization loop...
_canWrite = false;
serializer.Serialize(writer, value, type);
_canWrite = true;
return;
}
writer.WriteStartObject();
{
writer.WritePropertyName("$wrappedType");
writer.WriteValue(type.GetJsonSimpleTypeName());
writer.WritePropertyName("$wrappedValue");
converter.WriteJson(writer, value, serializer);
}
writer.WriteEndObject();
}
}

Change JSON.net content via property name?

I have this structure :
List<dynamic> lst = new List<dynamic>();
lst.Add(new{objId = 1,myOtherColumn = 5});
lst.Add(new{objId = 2,myOtherColumn = 6});
lst.Add(new{lala = "asd" ,lala2 = 7});
I'm serializing it via :
string st= JsonConvert.SerializeObject(lst);
Question:
How can I cause the serializer to change only values of the "objId" property , to something else ?
I know I should use class Myconverter : JsonConverter , but I didn't find any example which keeps the default behavior and in addition - allow me to add condition logic of serialization.
Here's a converter that will handle it, at least for simple objects as per your example. It looks for objects containing objId properties and then serialises all properties it finds on them. You may need to expand it to deal with other member types/more complex properties as required:
class MyConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteStartObject();
foreach (var prop in value.GetType().GetProperties()) {
writer.WritePropertyName(prop.Name);
if (prop.Name == "objId") {
//modify objId values for example
writer.WriteValue(Convert.ToInt32(prop.GetValue(value, null)) + 10);
} else {
writer.WriteValue(prop.GetValue(value, null));
}
}
writer.WriteEndObject();
}
public override bool CanConvert(Type objectType)
{
//only attempt to handle types that have an objId property
return (objectType.GetProperties().Count(p => p.Name == "objId") == 1);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Alternatively, you can use a converter that specifies it will only convert int types, then query where in the JSON path you are before doing any conversions. This has the benefit of not needing to deal with all the other members of the anonymous type.
class MyConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (writer.Path.EndsWith(".objId")) {
writer.WriteValue(Convert.ToInt32(value) + 10);
}
else {
writer.WriteValue(value);
}
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof (int);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}

Serialize root-level type differently than lower-level type

Is it possible in Json.NET to serialize a root-level type in the serialization hierarchy differently than a reference encountered at a lower level in the hierarchy?
For example, with this type
class Serialized {
public Serialized Serialized;
public int A;
}
in this setup
var serialized = new Serialized() { A = 1 };
var serialized2 = new Serialized() { A = 2 };
serialized.Serialized = serialized2;
string json = GetJson(serialized);
where json is
{
"A":1
"Serialized": {
"ref":2
}
}
Specifically, the root-level serialization should use the default serialization strategy and the lower-level ones should use a custom converter (or similar).
You need to define custom JsonConverter and decorate the Serialized property with it:
public class PropertyConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return true;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value == null) return;
var serialized = (Serialized)value;
writer.WriteStartObject();
writer.WritePropertyName("ref");
writer.WriteValue(serialized.A);
writer.WriteEndObject();
}
}
And your class:
class Serialized {
[JsonConverter(typeof(PropertyConverter))
public Serialized Serialized;
public int A;
}
This will work only for one level of nesting. If you need it to work of multiple nested levels, you need to modify the converter:
public class PropertyConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return true;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value == null) return;
var serialized = (Serialized)value;
writer.WriteStartObject();
writer.WritePropertyName("ref");
writer.WriteValue(serialized.A);
if(serialized.Serialized != null)
{
writer.WritePropertyName("nested");
writer.WriteValue(JsonConvert.SerializeObject(serialized.Serialized, new JsonConverter[] {new PropertyConverter()}));
}
writer.WriteEndObject();
}
}

Categories

Resources