I'm attempting to implement IDeserializationCallback using JSON.NET. I'm deserializing an object, and I would like to generate a list of all the objects which were deserialized which implement IDeserializationCallback, what would be the best way to do this? Does JSON.NET have any appropriate extension point to facilitate this? I have a (seemingly) working solution below, however it is quite ugly, so I'm convinced there must be a better way to do this. Any help is appreciated, thanks!
private static JsonSerializer serializer = new JsonSerializer();
static cctor()
{
serializer.Converters.Add(new DeserializationCallbackConverter());
}
public static T Deserialize<T>(byte[] data)
{
using (var reader = new JsonTextReader(new StreamReader(new MemoryStream(data))))
using (DeserializationCallbackConverter.NewDeserializationCallbackBlock(reader))
return serializer.Deserialize<T>(reader);
}
private class DeserializationCallbackConverter : JsonConverter
{
[ThreadStatic]
private static ScopedConverter currentConverter;
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)
{
return currentConverter.ReadJson(reader, objectType, serializer);
}
public override bool CanConvert(Type objectType)
{
return currentConverter == null ? false : currentConverter.CanConvert();
}
public override bool CanWrite
{
get { return false; }
}
public static IDisposable NewDeserializationCallbackBlock(JsonReader reader)
{
return new ScopedConverter(reader);
}
private class ScopedConverter : IDisposable
{
private JsonReader jsonReader;
private string currentPath;
private List<IDeserializationCallback> callbackObjects;
public ScopedConverter(JsonReader reader)
{
jsonReader = reader;
callbackObjects = new List<IDeserializationCallback>();
currentConverter = this;
}
public object ReadJson(JsonReader reader, Type objectType, JsonSerializer serializer)
{
var lastPath = currentPath;
currentPath = reader.Path;
var obj = serializer.Deserialize(reader, objectType);
currentPath = lastPath;
var dc = obj as IDeserializationCallback;
if (dc != null && callbackObjects != null)
callbackObjects.Add(dc);
return obj;
}
public bool CanConvert()
{
return jsonReader.Path != currentPath;
}
public void Dispose()
{
currentConverter = null;
foreach (var obj in callbackObjects)
obj.OnDeserialization(null);
}
}
}
You can create a custom contract resolver that adds an extra, artificial OnDeserialized callback that tracks creation of reference type objects. Here's one example:
public interface IObjectCreationTracker
{
void Add(object obj);
ICollection<object> CreatedObjects { get; }
}
public class ReferenceObjectCreationTracker : IObjectCreationTracker
{
public ReferenceObjectCreationTracker()
{
this.CreatedObjects = new HashSet<object>();
}
public void Add(object obj)
{
if (obj == null)
return;
var type = obj.GetType();
if (type.IsValueType || type == typeof(string))
return;
CreatedObjects.Add(obj);
}
public ICollection<object> CreatedObjects { get; private set; }
}
public class ObjectCreationTrackerContractResolver : DefaultContractResolver
{
readonly SerializationCallback callback = (o, context) =>
{
var tracker = context.Context as IObjectCreationTracker;
if (tracker != null)
tracker.Add(o);
};
protected override JsonContract CreateContract(Type objectType)
{
var contract = base.CreateContract(objectType);
contract.OnDeserializedCallbacks.Add(callback);
return contract;
}
}
And then use it as follows:
public static class JsonExtensions
{
public static T DeserializeWithTracking<T>(string json, out ICollection<object> objects)
{
var tracker = new ReferenceObjectCreationTracker();
var settings = new JsonSerializerSettings
{
ContractResolver = new ObjectCreationTrackerContractResolver(),
Context = new StreamingContext(StreamingContextStates.All, tracker),
// Add other settings as required.
TypeNameHandling = TypeNameHandling.Auto,
};
var obj = (T)JsonConvert.DeserializeObject<T>(json, settings);
objects = tracker.CreatedObjects;
return obj;
}
}
Note that this only returns instances of non-string reference types. Returning instances of value types is more problematic as there is no obvious way to distinguish between a value type that eventually gets embedded into a larger object via a property setter and one that is retained in the object graph as a boxed reference, e.g. as shown in this question. If the boxed value type eventually gets embedded in some larger object there is no way to retain a direct reference to it.
Also note the use of StreamingContext.Context to pass the tracker down into the callback.
You may want to cache the contract resolver for best performance.
Update
In answer to the updated question of how to implement IDeserializationCallback with Json.NET, the above should work for reference types. For value types that implement this interface, you could:
Call the method immediately in the OnDeserialized callback rather than deferring it until serialization is complete, or
Throw an exception indicating that IDeserializationCallback is not supported for structs.
Related
I want to wrap some properties in a JSON object with some metadata, regardless if it's null or not. However, my custom JsonConverter.WriteJson override is not called in case the property is null.
What I get when property is not null:
{"Prop":{"Version":1, "Object":{"Content":"abc"}}}
What I get when it's null:
{"Prop":null}
What I want when it's null:
{"Prop":{"Version":1, "Object":null}}
Due to WriteJson never being called for null values, I do not get the opportunity to control this behavior. Is there any way to force this?
Note that I want to know if this is possible to do with e.g converters or contractresolvers, I can't/don't want to change the MyContent or Wrap classes (see below).
class VersioningJsonConverter : JsonConverter
{
//Does not get called if value is null !!
public override void WriteJson(JsonWriter writer, Object value, JsonSerializer serializer)
{
writer.WriteStartObject();
writer.WritePropertyName("v");
writer.WriteValue(1);
writer.WritePropertyName("o");
if(value == null)
{
//never happens
writer.WriteNull();
}
else
{
writer.WriteStartObject();
writer.WritePropertyName("Content");
writer.WriteValue((value as MyContent).Content);
writer.WriteEndObject();
}
writer.WriteEndObject();
}
public override Object ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer)
=> throw new NotImplementedException();
public override Boolean CanConvert(Type objectType) => objectType == typeof(MyContent);
public override Boolean CanRead => false;
}
public class MyContent
{
public String Content {get;set;}
}
public class Wrap
{
public MyContent Prop {get;set;}
}
There is no way currently to make Json.NET call JsonConverter.WriteJson() for a null value. This can be seen in JsonSerializerInternalWriter.SerializeValue(...) which immediately writes a null and returns for a null incoming value:
private void SerializeValue(JsonWriter writer, object value, JsonContract valueContract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty)
{
if (value == null)
{
writer.WriteNull();
return;
}
// Remainder omitted
So if you need to translate null member(s) to non-null JSON value(s) but cannot modify the types themselves, you have two options:
Create a custom JsonConverter for the parent declaring type(s) of the member(s) that serializes every parent manually, OR
Create a custom contract resolver that translates the member(s) to ones returning some non-null surrogate or wrapper object.
Option #2 is more maintainable. The following contract resolver should do the job, wrapping the returned value of every member returning a value of the type(s) specified in the incoming list of types with the required version information:
public class CustomContractResolver : DefaultContractResolver
{
// Because contracts are cached, WrappedTypes must not be modified after construction.
readonly HashSet<Type> WrappedTypes = new HashSet<Type>();
public CustomContractResolver(IEnumerable<Type> wrappedTypes)
{
if (wrappedTypes == null)
throw new ArgumentNullException();
foreach (var type in wrappedTypes)
WrappedTypes.Add(type);
}
class VersionWrapperProvider<T> : IValueProvider
{
readonly IValueProvider baseProvider;
public VersionWrapperProvider(IValueProvider baseProvider)
{
if (baseProvider == null)
throw new ArgumentNullException();
this.baseProvider = baseProvider;
}
public object GetValue(object target)
{
return new VersionWrapper<T>(target, baseProvider);
}
public void SetValue(object target, object value) { }
}
class ReadOnlyVersionWrapperProvider<T> : IValueProvider
{
readonly IValueProvider baseProvider;
public ReadOnlyVersionWrapperProvider(IValueProvider baseProvider)
{
if (baseProvider == null)
throw new ArgumentNullException();
this.baseProvider = baseProvider;
}
public object GetValue(object target)
{
return new ReadOnlyVersionWrapper<T>(target, baseProvider);
}
public void SetValue(object target, object value) { }
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty property = base.CreateProperty(member, memberSerialization);
if (WrappedTypes.Contains(property.PropertyType)
&& !(member.DeclaringType.IsGenericType
&& (member.DeclaringType.GetGenericTypeDefinition() == typeof(VersionWrapper<>) || member.DeclaringType.GetGenericTypeDefinition() == typeof(ReadOnlyVersionWrapper<>))))
{
var wrapperGenericType = (property.Writable ? typeof(VersionWrapper<>) : typeof(ReadOnlyVersionWrapper<>));
var providerGenericType = (property.Writable ? typeof(VersionWrapperProvider<>) : typeof(ReadOnlyVersionWrapperProvider<>));
var wrapperType = wrapperGenericType.MakeGenericType(new[] { property.PropertyType });
var providerType = providerGenericType.MakeGenericType(new[] { property.PropertyType });
property.PropertyType = wrapperType;
property.ValueProvider = (IValueProvider)Activator.CreateInstance(providerType, property.ValueProvider);
property.ObjectCreationHandling = ObjectCreationHandling.Reuse;
}
return property;
}
}
internal class VersionWrapper<T>
{
readonly object target;
readonly IValueProvider baseProvider;
public VersionWrapper(object target, IValueProvider baseProvider)
{
this.target = target;
this.baseProvider = baseProvider;
}
public int Version { get { return 1; } }
[JsonProperty(NullValueHandling = NullValueHandling.Include)]
public T Object
{
get
{
return (T)baseProvider.GetValue(target);
}
set
{
baseProvider.SetValue(target, value);
}
}
}
internal class ReadOnlyVersionWrapper<T>
{
readonly object target;
readonly IValueProvider baseProvider;
public ReadOnlyVersionWrapper(object target, IValueProvider baseProvider)
{
this.target = target;
this.baseProvider = baseProvider;
}
public int Version { get { return 1; } }
[JsonProperty(NullValueHandling = NullValueHandling.Include)]
public T Object
{
get
{
return (T)baseProvider.GetValue(target);
}
}
}
Then use it as follows to wrap all properties of type MyContent:
static IContractResolver resolver = new CustomContractResolver(new[] { typeof(MyContent) });
// And later
var settings = new JsonSerializerSettings
{
ContractResolver = resolver,
};
var json = JsonConvert.SerializeObject(wrap, Formatting.Indented, settings);
Notes:
You should statically cache the contract resolver for performance reasons explained here.
VersionWrapperProvider<T> creates a wrapper object with the necessary version information as well as a surrogate Object property that gets and sets the underlying value using Json.NET's own IValueProvider.
Because Json.NET does not set back the value of a pre-allocated reference property, but instead simply populates it with the deserialized property values, it is necessary for the setter of VersionWrapper<T>.Object to itself set the value in the parent.
If your wrapped types are polymorphic, in CreateProperty() you may need to check whether any of the base types of property.PropertyType are in WrappedTypes.
Populating a pre-existing Wrap using JsonConvert.PopulateObject should be tested.
This solution may not work when deserializing properties passed to parameterized constructors. DefaultContractResolver.CreatePropertyFromConstructorParameter would need modification in such a situation.
Working sample .Net fiddle here.
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);
}
}
}
Using Json.NET, I'd like to map a JObject onto a .NET object with the following behavior:
For (non-null) array properties of the JObject, replace the entire collection on the target object.
For (non-null) object properties of the JObject, reuse the target object's property if it's not null, and map just the provided properties onto it.
JsonSerializer.Populate seems to be what I want, as described in this answer. As for the behaviors I'm looking for, it seems I can achieve one or the other, but not both, via JsonSerializerSettings.ObjectCreationHandling. ObjectCreationHandling.Replace does what I want with respect to requirement #1, while ObjectCreationHandling.Auto does what I want with respect to requirement #2, but it appends array items onto the existing collection.
What is the recommended way to achieve both requirements here?
Json.NET will automatically replace any arrays or read-only collections. To clear out read-write collections when deserializing, you could create a custom contract resolver that adds an OnDeserializingCallback to every modifiable collection that clears the collection when deserialization begins. Clearing the collection rather that replacing it outright handles cases where the collection is get-only, for instance:
public class RootObject
{
readonly HashSet<int> hashSet = new HashSet<int>();
public HashSet<int> HashSetValues { get { return this.hashSet; } }
}
The contract resolver is as follows:
public class CollectionClearingContractResolver : DefaultContractResolver
{
static void ClearGenericCollectionCallback<T>(object o, StreamingContext c)
{
var collection = o as ICollection<T>;
if (collection == null || collection is Array || collection.IsReadOnly)
return;
collection.Clear();
}
static SerializationCallback ClearListCallback = (o, c) =>
{
var collection = o as IList;
if (collection == null || collection is Array || collection.IsReadOnly)
return;
collection.Clear();
};
protected override JsonArrayContract CreateArrayContract(Type objectType)
{
var contract = base.CreateArrayContract(objectType);
if (!objectType.IsArray)
{
if (typeof(IList).IsAssignableFrom(objectType))
{
contract.OnDeserializingCallbacks.Add(ClearListCallback);
}
else if (objectType.GetCollectItemTypes().Count() == 1)
{
MethodInfo method = typeof(CollectionClearingContractResolver).GetMethod("ClearGenericCollectionCallback", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
MethodInfo generic = method.MakeGenericMethod(contract.CollectionItemType);
contract.OnDeserializingCallbacks.Add((SerializationCallback)Delegate.CreateDelegate(typeof(SerializationCallback), generic));
}
}
return contract;
}
}
public static class TypeExtensions
{
public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
{
if (type == null)
throw new ArgumentNullException();
if (type.IsInterface)
return new[] { type }.Concat(type.GetInterfaces());
else
return type.GetInterfaces();
}
public static IEnumerable<Type> GetCollectItemTypes(this Type type)
{
foreach (Type intType in type.GetInterfacesAndSelf())
{
if (intType.IsGenericType
&& intType.GetGenericTypeDefinition() == typeof(ICollection<>))
{
yield return intType.GetGenericArguments()[0];
}
}
}
}
public static class JsonExtensions
{
public static void Populate<T>(this JToken value, T target) where T : class
{
value.Populate(target, null);
}
public static void Populate<T>(this JToken value, T target, JsonSerializerSettings settings) where T : class
{
using (var sr = value.CreateReader())
{
JsonSerializer.CreateDefault(settings).Populate(sr, target);
}
}
}
Then to use it, do:
var settings = new JsonSerializerSettings
{
ContractResolver = new CollectionClearingContractResolver(),
};
jObject.Populate(rootObject, settings);
Sample fiddle.
Such a contract resolver would also be useful when deserializing objects that populate collections in their default constructor, as in Deserialization causes copies of List-Entries.
One fix is to use a custom JsonConverter that effectively replaces collections by ignoring the existing value when a collection type is detected.
public class ReplaceArrayConverter : JsonConverter
{
public override bool CanConvert(Type objectType) {
// check for Array, IList, etc.
return objectType.IsCollection();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
// ignore existingValue and just create a new collection
return JsonSerializer.CreateDefault().Deserialize(reader, objectType);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
JsonSerializer.CreateDefault().Serialize(writer, value);
}
}
Used like so:
var ser = JsonSerializer.CreateDefault(new JsonSerializerSettings {
Converters = new[] { new ReplaceArrayConverter() }
});
using (var reader = jObj.CreateReader()) {
ser.Populate(reader, model);
}
I'm using Json.Net to serialize some application data. Of course, the application specs have slightly changed and we need to refactor some of the business object data. What are some viable strategies to migrate previously serialized data to our new data format?
For example, say we have orignally had a business object like:
public class Owner
{
public string Name {get;set;}
}
public class LeaseInstrument
{
public ObservableCollection<Owner> OriginalLessees {get;set;}
}
We serialize an instance of a LeaseInstrument to a file with Json.Net. Now, we change our business objects to look like:
public class Owner
{
public string Name {get;set;}
}
public class LeaseOwner
{
public Owner Owner { get;set;}
public string DocumentName {get;set;}
}
public class LeaseInstrument
{
public ObservableCollection<LeaseOwner> OriginalLessees {get;set;}
}
I have looked into writing a custom JsonConverter for LeaseInstrument, but the ReadJson method is not ever hit...instead an exception is thrown before the deserializer reaches that point:
Additional information: Type specified in JSON
'System.Collections.ObjectModel.ObservableCollection`1[[BreakoutLib.BO.Owner,
BreakoutLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]],
System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
is not compatible with 'System.Collections.ObjectModel.ObservableCollection`1[[BreakoutLib.BO.LeaseOwner, BreakoutLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'. Path 'Is.$values[8].OriginalLessors.$type', line 3142, position 120.
I mean, no joke, Json.Net, that's why I'm trying to run a JsonConverter when deserializing these objects, so I can manually handle the fact that the serialized type doesn't match the compiled type!!
For what it's worth, here are the JsonSerializerSettings we are using:
var settings = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
ContractResolver = new WritablePropertiesOnlyResolver(),
TypeNameHandling = TypeNameHandling.All,
ObjectCreationHandling = ObjectCreationHandling.Reuse
};
You have the following issues:
You serialized using TypeNameHandling.All. This setting serializes type information for collections as well as objects. I don't recommend doing this. Instead I suggest using TypeNameHandling.Objects and then letting the deserializing system choose the collection type.
That being said, to deal with your existing JSON, you can adapt the IgnoreArrayTypeConverter from make Json.NET ignore $type if it's incompatible to use with a resizable collection:
public class IgnoreCollectionTypeConverter : JsonConverter
{
public IgnoreCollectionTypeConverter() { }
public IgnoreCollectionTypeConverter(Type ItemConverterType)
{
this.ItemConverterType = ItemConverterType;
}
public Type ItemConverterType { get; set; }
public override bool CanConvert(Type objectType)
{
// TODO: test with read-only collections.
return objectType.GetCollectItemTypes().Count() == 1 && !objectType.IsDictionary() && !objectType.IsArray;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (!CanConvert(objectType))
throw new JsonSerializationException(string.Format("Invalid type \"{0}\"", objectType));
if (reader.TokenType == JsonToken.Null)
return null;
var token = JToken.Load(reader);
var itemConverter = (ItemConverterType == null ? null : (JsonConverter)Activator.CreateInstance(ItemConverterType, true));
if (itemConverter != null)
serializer.Converters.Add(itemConverter);
try
{
return ToCollection(token, objectType, existingValue, serializer);
}
finally
{
if (itemConverter != null)
serializer.Converters.RemoveLast(itemConverter);
}
}
private static object ToCollection(JToken token, Type collectionType, object existingValue, JsonSerializer serializer)
{
if (token == null || token.Type == JTokenType.Null)
return null;
else if (token.Type == JTokenType.Array)
{
// Here we assume that existingValue already is of the correct type, if non-null.
existingValue = serializer.DefaultCreate<object>(collectionType, existingValue);
token.PopulateObject(existingValue, serializer);
return existingValue;
}
else if (token.Type == JTokenType.Object)
{
var values = token["$values"];
if (values == null)
return null;
return ToCollection(values, collectionType, existingValue, serializer);
}
else
{
throw new JsonSerializationException("Unknown token type: " + token.ToString());
}
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
You need to upgrade your Owner to a LeaseOwner.
You can write a JsonConverter for this purpose that loads the relevant portion of JSON into a JObject, then checks to see whether the object looks like one from the old data model, or the new. If the JSON looks old, map fields as necessary using Linq to JSON. If the JSON object looks new, you can just populate your LeaseOwner with it.
Since you are setting PreserveReferencesHandling = PreserveReferencesHandling.Objects the converter will need to handle the "$ref" properties manually:
public class OwnerToLeaseOwnerConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(LeaseOwner).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var item = JObject.Load(reader);
if (item["$ref"] != null)
{
var previous = serializer.ReferenceResolver.ResolveReference(serializer, (string)item["$ref"]);
if (previous is LeaseOwner)
return previous;
else if (previous is Owner)
{
var leaseOwner = serializer.DefaultCreate<LeaseOwner>(objectType, existingValue);
leaseOwner.Owner = (Owner)previous;
return leaseOwner;
}
else
{
throw new JsonSerializationException("Invalid type of previous object: " + previous);
}
}
else
{
var leaseOwner = serializer.DefaultCreate<LeaseOwner>(objectType, existingValue);
if (item["Name"] != null)
{
// Convert from Owner to LeaseOwner. If $id is present, this stores the reference mapping in the reference table for us.
leaseOwner.Owner = item.ToObject<Owner>(serializer);
}
else
{
// PopulateObject. If $id is present, this stores the reference mapping in the reference table for us.
item.PopulateObject(leaseOwner, serializer);
}
return leaseOwner;
}
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
These use the extensions:
public static class JsonExtensions
{
public static T DefaultCreate<T>(this JsonSerializer serializer, Type objectType, object existingValue)
{
if (serializer == null)
throw new ArgumentNullException();
if (existingValue is T)
return (T)existingValue;
return (T)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
}
public static void PopulateObject(this JToken obj, object target, JsonSerializer serializer)
{
if (target == null)
throw new NullReferenceException();
if (obj == null)
return;
using (var reader = obj.CreateReader())
serializer.Populate(reader, target);
}
}
public static class TypeExtensions
{
/// <summary>
/// Return all interfaces implemented by the incoming type as well as the type itself if it is an interface.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
{
if (type == null)
throw new ArgumentNullException();
if (type.IsInterface)
return new[] { type }.Concat(type.GetInterfaces());
else
return type.GetInterfaces();
}
public static IEnumerable<Type> GetCollectItemTypes(this Type type)
{
foreach (Type intType in type.GetInterfacesAndSelf())
{
if (intType.IsGenericType
&& intType.GetGenericTypeDefinition() == typeof(ICollection<>))
{
yield return intType.GetGenericArguments()[0];
}
}
}
public static bool IsDictionary(this Type type)
{
if (typeof(IDictionary).IsAssignableFrom(type))
return true;
foreach (Type intType in type.GetInterfacesAndSelf())
{
if (intType.IsGenericType
&& intType.GetGenericTypeDefinition() == typeof(IDictionary<,>))
{
return true;
}
}
return false;
}
}
public static class ListExtensions
{
public static bool RemoveLast<T>(this IList<T> list, T item)
{
if (list == null)
throw new ArgumentNullException();
var comparer = EqualityComparer<T>.Default;
for (int i = list.Count - 1; i >= 0; i--)
{
if (comparer.Equals(list[i], item))
{
list.RemoveAt(i);
return true;
}
}
return false;
}
}
You can apply the converters directly to your data model using JsonConverterAttribute, like so:
public class LeaseInstrument
{
[JsonConverter(typeof(IgnoreCollectionTypeConverter), typeof(OwnerToLeaseOwnerConverter))]
public ObservableCollection<LeaseOwner> OriginalLessees { get; set; }
}
If you don't want to have a dependency on Json.NET in your data model, you can do this in your custom contract resolver:
public class WritablePropertiesOnlyResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var result = base.CreateProperty(member, memberSerialization);
if (typeof(LeaseInstrument).IsAssignableFrom(result.DeclaringType) && typeof(ICollection<LeaseOwner>).IsAssignableFrom(result.PropertyType))
{
var converter = new IgnoreCollectionTypeConverter { ItemConverterType = typeof(OwnerToLeaseOwnerConverter) };
result.Converter = result.Converter ?? converter;
result.MemberConverter = result.MemberConverter ?? converter;
}
return result;
}
}
Incidentally, you might want to cache your custom contract resolver for best performance.
You might find our library Migrations.Json.Net helpful
https://github.com/Weingartner/Migrations.Json.Net
A Simple example. Say you start with a class
public class Person {
public string Name {get;set}
}
and then you want to migrate to
public class Person {
public string FirstName {get;set}
public string SecondName {get;set}
public string Name => $"{FirstName} {SecondName}";
}
you would perhaps do the following migration
public class Person {
public string FirstName {get;set}
public string SecondName {get;set}
public string Name => $"{FirstName} {SecondName}";
public void migrate_1(JToken token, JsonSerializer s){
var name = token["Name"];
var names = names.Split(" ");
token["FirstName"] = names[0];
token["SecondName"] = names[1];
return token;
}
}
The above glosses over some details but there is a full example on the homepage of the project. We use this extensively in two of our production projects. The example on the homepage has 13 migrations to a complex object that has changed over several years.
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();
}
}