It is possible to define a custom "list-merge" startegy used for the JsonConvert.PopulateObject method?
Example:
I have two models:
class Parent
{
public Guid Uuid { get; set; }
public string Name { get; set; }
public List<Child> Childs { get; set; }
}
class Child
{
public Guid Uuid { get; set; }
public string Name { get; set; }
public int Score { get; set; }
}
My initial JSON:
{
"Uuid":"cf82b1fd-1ca0-4125-9ea2-43d1d71c9bed",
"Name":"John",
"Childs":[
{
"Uuid":"96b93f95-9ce9-441d-bfb0-f44b65f7fe0d",
"Name":"Philip",
"Score":100
},
{
"Uuid":"fe7837e0-9960-4c45-b5ab-4e4658c08ccd",
"Name":"Peter",
"Score":150
},
{
"Uuid":"1d2cdba4-9efb-44fc-a2f3-6b86a5291954",
"Name":"Steve",
"Score":80
}
]
}
and my update JSON:
{
"Uuid":"cf82b1fd-1ca0-4125-9ea2-43d1d71c9bed",
"Childs":[
{
"Uuid":"fe7837e0-9960-4c45-b5ab-4e4658c08ccd",
"Score":170
}
]
}
All I need is to specify a model property (by attribute) used for matching list items (in my case the Uuid property of Child), so calling the JsonConvert.PopulateObject on the object deserialized from my initial JSON with a update JSON (it contains ONLY changed values + Uuids for every object) results to update only list elements contained in the update JSON macthed by Uuid (in my case update a Peter's score) and elements not contained in the update JSON leave without change.
I'm searching for some universal solution - I need to apply it on large JSONs with a lot of nested lists (but every model has some unique property). So I need to recursively call PopulateObject on matched list item.
You could create your own JsonConverter that implements the required merge logic. This is possible because JsonConverter.ReadJson is passed an existingValue parameter that contains the pre-existing contents of the property being deserialized.
Thus:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class JsonMergeKeyAttribute : System.Attribute
{
}
public class KeyedListMergeConverter : JsonConverter
{
readonly IContractResolver contractResolver;
public KeyedListMergeConverter(IContractResolver contractResolver)
{
if (contractResolver == null)
throw new ArgumentNullException("contractResolver");
this.contractResolver = contractResolver;
}
static bool CanConvert(IContractResolver contractResolver, Type objectType, out Type elementType, out JsonProperty keyProperty)
{
elementType = objectType.GetListType();
if (elementType == null)
{
keyProperty = null;
return false;
}
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 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");
Type elementType;
JsonProperty keyProperty;
if (!CanConvert(contractResolver, objectType, out elementType, out keyProperty))
throw new JsonSerializationException(string.Format("Invalid input type {0}", objectType));
if (reader.TokenType == JsonToken.Null)
return existingValue;
var list = existingValue as IList;
if (list == null || list.Count == 0)
{
list = list ?? (IList)contractResolver.ResolveContract(objectType).DefaultCreator();
serializer.Populate(reader, list);
}
else
{
var jArray = JArray.Load(reader);
var comparer = new KeyedListMergeComparer();
var lookup = jArray.ToLookup(i => i[keyProperty.PropertyName].ToObject(keyProperty.PropertyType, serializer), comparer);
var done = new HashSet<JToken>();
foreach (var item in list)
{
var key = keyProperty.ValueProvider.GetValue(item);
var replacement = lookup[key].Where(v => !done.Contains(v)).FirstOrDefault();
if (replacement != null)
{
using (var subReader = replacement.CreateReader())
serializer.Populate(subReader, item);
done.Add(replacement);
}
}
// Populate the NEW items into the list.
if (done.Count < jArray.Count)
foreach (var item in jArray.Where(i => !done.Contains(i)))
{
list.Add(item.ToObject(elementType, serializer));
}
}
return list;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
class KeyedListMergeComparer : IEqualityComparer<object>
{
#region IEqualityComparer<object> Members
bool IEqualityComparer<object>.Equals(object x, object y)
{
if (object.ReferenceEquals(x, y))
return true;
else if (x == null || y == null)
return false;
return x.Equals(y);
}
int IEqualityComparer<object>.GetHashCode(object obj)
{
if (obj == null)
return 0;
return obj.GetHashCode();
}
#endregion
}
}
public static class TypeExtensions
{
public static Type GetListType(this Type type)
{
while (type != null)
{
if (type.IsGenericType)
{
var genType = type.GetGenericTypeDefinition();
if (genType == typeof(List<>))
return type.GetGenericArguments()[0];
}
type = type.BaseType;
}
return null;
}
}
Notice that the converter needs to know the IContractResolver currently in use. Having it makes finding the key parameter easier, and also ensures that, if the key parameter has a [JsonProperty(name)] attribute, the replacement name is respected.
Then add the attribute:
class Child
{
[JsonMergeKey]
[JsonProperty("Uuid")] // Replacement name for testing
public Guid UUID { get; set; }
public string Name { get; set; }
public int Score { get; set; }
}
And use the converter as follows:
var serializer = JsonSerializer.CreateDefault();
var converter = new KeyedListMergeConverter(serializer.ContractResolver);
serializer.Converters.Add(converter);
using (var reader = new StringReader(updateJson))
{
serializer.Populate(reader, parent);
}
The converter assumes that the key parameter is always present in the JSON. Also, if any entries in the JSON being merged have keys that are not found in the existing list, they are appended to the list.
Update
The original converter is specifically hardcoded for List<T>, and takes advantage of the fact that List<T> implements both IList<T> and IList. If your collection is not a List<T> but still implements IList<T>, the following should work:
public class KeyedIListMergeConverter : JsonConverter
{
readonly IContractResolver contractResolver;
public KeyedIListMergeConverter(IContractResolver contractResolver)
{
if (contractResolver == null)
throw new ArgumentNullException("contractResolver");
this.contractResolver = contractResolver;
}
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 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");
Type elementType;
JsonProperty keyProperty;
if (!CanConvert(contractResolver, objectType, out elementType, out keyProperty))
throw new JsonSerializationException(string.Format("Invalid input type {0}", objectType));
if (reader.TokenType == JsonToken.Null)
return existingValue;
var method = GetType().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)
{
var list = existingValue as IList<T>;
if (list == null || list.Count == 0)
{
list = list ?? (IList<T>)contractResolver.ResolveContract(objectType).DefaultCreator();
serializer.Populate(reader, list);
}
else
{
var jArray = JArray.Load(reader);
var comparer = new KeyedListMergeComparer();
var lookup = jArray.ToLookup(i => i[keyProperty.PropertyName].ToObject(keyProperty.PropertyType, serializer), comparer);
var done = new HashSet<JToken>();
foreach (var item in list)
{
var key = keyProperty.ValueProvider.GetValue(item);
var replacement = lookup[key].Where(v => !done.Contains(v)).FirstOrDefault();
if (replacement != null)
{
using (var subReader = replacement.CreateReader())
serializer.Populate(subReader, item);
done.Add(replacement);
}
}
// Populate the NEW items into the list.
if (done.Count < jArray.Count)
foreach (var item in jArray.Where(i => !done.Contains(i)))
{
list.Add(item.ToObject<T>(serializer));
}
}
return list;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
class KeyedListMergeComparer : IEqualityComparer<object>
{
#region IEqualityComparer<object> Members
bool IEqualityComparer<object>.Equals(object x, object y)
{
return object.Equals(x, y);
}
int IEqualityComparer<object>.GetHashCode(object obj)
{
if (obj == null)
return 0;
return obj.GetHashCode();
}
#endregion
}
}
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> GetIListItemTypes(this Type type)
{
foreach (Type intType in type.GetInterfacesAndSelf())
{
if (intType.IsGenericType
&& intType.GetGenericTypeDefinition() == typeof(IList<>))
{
yield return intType.GetGenericArguments()[0];
}
}
}
}
Note that merging is not implemented for arrays since they are not resizable.
Related
I have an asp.net core webservice which needs to handle interface-properties of an object correctly (has to find out which implementation to use for deserialization of the json-string).
for that reason I've created an class which gets called from an interface-Attribute, like:
[JsonConverter(typeof(InterfaceJsonConverter<IInputFormat>))]
public interface IInputFormat
{
// ...
}
The model-class where the InterfaceJsonConverter is defined has to be .NET Standard 1.4 which doesn't Support assembly-loading.
But this technique is used to find out which assembly has an implementation of the Interface and which implementation is the closest to the json-object.
(I could of course add the Interfaces directly to the class instead of reading the assemblies but I want a dynamic-solution that I will never miss an implementation)
(originally I developed that in .net 4.6 and it worked great there).
So what I'm trying to do now is :
Create a new .NET Standard 1.6 Library
Put the InterfaceJsonConverter in that library
Remove the Attributes from the Interface(s)
Add Interface-Attributes at runtime...
Is there any way to archive this?
Or are there better ways to handle Interfaces in asp.net core?
just for completeness I will post the Code for InterfaceJsonConverter
public class InterfaceJsonConverter<T> : Newtonsoft.Json.JsonConverter
{
public InterfaceJsonConverter()
{
this.DerivedTypes = GetTypesOfImplementedInterfaces(typeof(T));
}
readonly HashSet<Type> derivedTypes = new HashSet<Type>();
public InterfaceJsonConverter(params Type[] types)
{
this.DerivedTypes = types;
}
public InterfaceJsonConverter(System.Reflection.Assembly assembly, string #namespace)
{
this.DerivedTypes = GetTypesInNamespace(assembly, #namespace);
}
private Type[] GetTypesOfImplementedInterfaces(Type type)
{
var parts = type.AssemblyQualifiedName.Split(',');
var assemblyName = parts[1].Trim();
var assemblies = AssemblyRepo.GetReferencingAssemblies(assemblyName);
//var assemblies = AppDomain.CurrentDomain.GetAssemblies();
var types = new List<Type>();
foreach (var a in assemblies)
{
try
{
//type.GetInterfaces().Any(i => i.FullName == typeof(T).FullName)
var currentTypes = a.GetTypes().Where(t => t.GetTypeInfo().IsAbstract==false && t.GetInterfaces().Any(i => i.FullName == type.FullName)).ToList();
types.AddRange(currentTypes);
}
catch (System.Exception) { }// ignored}
}
if(types.Count==0)
throw new System.Exception("No class found which implements interface [" + typeof(T) + "].");
return types.ToArray();
}
private Type[] GetTypesInNamespace(System.Reflection.Assembly assembly, string #namespace)
{
//return assembly.GetTypes().Where(t => String.Equals(t.Namespace, nameSpace, StringComparison.Ordinal)).ToArray();
return assembly.GetTypes()
.Where(
type => String.Equals(type.Namespace, #namespace)
&& type.GetInterfaces().Any(i => i.FullName == typeof (T).FullName)
).ToArray();
}
public IEnumerable<Type> DerivedTypes
{
get
{
return derivedTypes.ToArray();
}
set
{
if (value == null)
throw new ArgumentNullException();
derivedTypes.Clear();
derivedTypes.UnionWith(value);
}
}
JsonObjectContract FindContract(JObject obj, JsonSerializer serializer)
{
List<JsonObjectContract> bestContracts = new List<JsonObjectContract>();
foreach (var type in derivedTypes)
{
if (type.GetTypeInfo().IsAbstract)
continue;
var contract = serializer.ContractResolver.ResolveContract(type) as JsonObjectContract;
if (contract == null)
continue;
if (obj.Properties().Select(p => p.Name).Where(n => n != "$type").Any(n => contract.Properties.GetClosestMatchProperty(n) == null))
continue;
if (bestContracts.Count == 0 || bestContracts[0].Properties.Count > contract.Properties.Count)
{
bestContracts.Clear();
bestContracts.Add(contract);
}
else if (contract.Properties.Count == bestContracts[0].Properties.Count)
{
bestContracts.Add(contract);
}
}
return bestContracts.Count > 0 ? bestContracts.Single() : null;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(T);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var obj = JObject.Load(reader); // Throws an exception if the current token is not an object.
if (obj["$type"] != null && serializer.TypeNameHandling != TypeNameHandling.None)
{
// Prevent infinite recursion when using an explicit converter in the list.
var removed = serializer.Converters.Remove(this);
try
{
// Kludge to prevent infinite recursion when using JsonConverterAttribute on the type: deserialize to object.
return obj.ToObject(typeof (object), serializer);
}
finally
{
if (removed)
serializer.Converters.Add(this);
}
}
var contract = FindContract(obj, serializer);
if (contract == null)
throw new JsonSerializationException("no contract found for " + obj.ToString());
if (existingValue == null || !contract.UnderlyingType.IsInstanceOfType(existingValue))
//!contract.UnderlyingType.IsAssignableFrom(existingValue.GetType()))
existingValue = contract.DefaultCreator();
//will call the default constructor(PARAMETERLESS)...MAKE SURE YOUR CLASS HAS ONE!!!!!
using (var sr = obj.CreateReader())
{
serializer.Populate(sr, existingValue);
}
return existingValue;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
When my API sends back a single object, we'll call it Item (A GetEntity in OData terms), it looks like this:
{
"d" : {
"Item" : "123456",
"OldItem" : "78921",
}
}
When I grab a set of the same object, i.e. returning List of Item, I get:
{
"d":{
"results":[
{
"Item":"343431",
"OldItem":"21314"
},
{
"Item":"341321",
"OldItem":"43563"
}
]
}
}
Other than the obvious "d" base node I need to get rid of, I'm having trouble attempting to use the same class in C# to do this. I have an Item class as such:
public class Material : IEntity
{
[JsonProperty("Item")]
public string material_number { get; set; }
[JsonProperty("OldItem")]
public string old_material_number { get; set; }
// Methods
public Material() {}
public bool Validate() { throw new NotImplementedException(); }
}
I'd like to be able to call a custom JsonConverter to handle this, but I haven't been able to get some of the example out there for single object, array converters to work. Ideally, I should be able to call:
JsonConvert.DeserializeObject<T> where T is either Material or List<Material>. How can I build a JsonConverter to handle both scenarios?
I'm calling the JsonConvert.DeserializeObject<T> as such:
if (response.IsSuccessStatusCode)
{
return JsonConvert.DeserializeObject<T>(JObject.Parse(response.Content.ReadAsStringAsync().Result).SelectToken("d").ToString());
}
else
{
throw new Exception("Service Error");
}
Here is a JsonConverter that should work for your situation. Note that if there are any other JSON formats you could receive that are not shown in your question -- for example, if d can have a value of null when there are no results -- you may need to make adjustments to the converter. Currently it will throw an exception if it encounters something it does not expect, but you could make it return null or an empty list instead, if you prefer.
public class MaterialArrayConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
if (token.Type == JTokenType.Object)
{
JToken results = token["results"];
if (results != null && results.Type == JTokenType.Array)
{
// we've got multiple items; deserialize to a list
return results.ToObject<List<Material>>(serializer);
}
else if (results == null)
{
// "results" property not present; return a list of one item
return new List<Material> { token.ToObject<Material>(serializer) };
}
}
// some other format we're not expecting
throw new JsonSerializationException("Unexpected JSON format encountered in MaterialArrayConverter: " + token.ToString());
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override bool CanWrite
{
get { return false; }
}
public override bool CanConvert(Type objectType)
{
// CanConvert is not called when [JsonConverter] attribute is used
return false;
}
}
You can use it by making a RootObject class annotated as shown below and then deserializing your JSON into that:
public class RootObject
{
[JsonProperty("d")]
[JsonConverter(typeof(MaterialArrayConverter))]
public List<Material> Materials { get; set; }
}
Then:
var root = JsonConvert.DeserializeObject<RootObject>(json);
From there you can retrieve the list of materials and use it as you see fit.
Fiddle: https://dotnetfiddle.net/tKb6Ke
Assuming you want to return your material(s) in a collection, you can use the following generic JsonConverter
public class SingleOrResultListConverter<T> : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(ICollection<T>).IsAssignableFrom(objectType);
}
const string Results = "results";
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
if (objectType.IsArray)
{
var list = (List<T>)ReadJson(reader, typeof(List<T>), new List<T>(), serializer);
return list.ToArray();
}
else
{
var list = (ICollection<T>)(existingValue ?? serializer.ContractResolver.ResolveContract(objectType).DefaultCreator());
if (reader.TokenType == JsonToken.StartArray)
{
serializer.Populate(reader, list);
}
else if (reader.TokenType == JsonToken.StartObject)
{
JObject obj = null;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.PropertyName:
string propertyName = reader.Value.ToString();
if (!reader.Read())
{
throw new JsonSerializationException("Unexpected end while reading collection");
}
if (propertyName == Results)
{
serializer.Populate(reader, list);
}
else
{
obj = obj ?? new JObject();
obj[propertyName] = JToken.Load(reader);
}
break;
case JsonToken.Comment:
break;
case JsonToken.EndObject:
if (obj != null)
list.Add(obj.ToObject<T>(serializer));
return list;
}
}
throw new JsonSerializationException("Unexpected end while reading collection");
}
else
{
throw new JsonSerializationException("Unexpected start token: " + reader.TokenType);
}
return list;
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var collection = (ICollection<T>)value;
if (collection.Count == 1)
{
serializer.Serialize(writer, collection.First());
}
else
{
writer.WriteStartObject();
writer.WritePropertyName(Results);
writer.WriteStartArray();
foreach (var item in collection)
{
serializer.Serialize(writer, item);
}
writer.WriteEndArray();
writer.WriteEndObject();
}
}
}
Then use it as follows:
var obj = JObject.Parse(json);
var subObj = obj["d"] ?? obj; // Strip the "d".
var settings = new JsonSerializerSettings { Converters = new[] { new SingleOrResultListConverter<Material>() } };
var list = subObj.ToObject<List<Material>>(JsonSerializer.CreateDefault(settings));
Or if you prefer an array to a List<T>:
var array = subObj.ToObject<Material[]>(JsonSerializer.CreateDefault(settings));
Prototype fiddle.
Update
You need to make sure you are allocating and passing your converter to Json.NET. Since your deserialization method is generic, you'll need to do something generic, like so:
var json = response.Content.ReadAsStringAsync().Result;
var converters = typeof(T).GetCollectionItemTypes()
.Select(t => (JsonConverter)Activator.CreateInstance(typeof(SingleOrResultListConverter<>).MakeGenericType(new [] { t })))
.ToArray();
var settings = new JsonSerializerSettings { Converters = converters };
return JToken.Parse(json).SelectToken("d").ToObject<T>(JsonSerializer.CreateDefault(settings));
Using the extension method:
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> GetCollectionItemTypes(this Type type)
{
foreach (Type intType in type.GetInterfacesAndSelf())
{
if (intType.IsGenericType
&& intType.GetGenericTypeDefinition() == typeof(ICollection<>))
{
yield return intType.GetGenericArguments()[0];
}
}
}
}
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>
}
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.
How could i deserialize json into a List of enum in C#?
I wrote the following code:
//json "types" : [ "hotel", "spa" ]
public enum eType
{
[Description("hotel")]
kHotel,
[Description("spa")]
kSpa
}
public class HType
{
List<eType> m_types;
[JsonProperty("types")]
public List<eType> HTypes {
get
{
return m_types;
}
set
{
// i did this to try and decide in the setter
// what enum value should be for each type
// making use of the Description attribute
// but throws an exception
}
}
}
//other class
var hTypes = JsonConvert.DeserializeObject<HType>(json);
A custom converter may help.
var hType = JsonConvert.DeserializeObject<HType>(
#"{""types"" : [ ""hotel"", ""spa"" ]}",
new MyEnumConverter());
public class HType
{
public List<eType> types { set; get; }
}
public enum eType
{
[Description("hotel")]
kHotel,
[Description("spa")]
kSpa
}
public class MyEnumConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(eType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var eTypeVal = typeof(eType).GetMembers()
.Where(x => x.GetCustomAttributes(typeof(DescriptionAttribute)).Any())
.FirstOrDefault(x => ((DescriptionAttribute)x.GetCustomAttribute(typeof(DescriptionAttribute))).Description == (string)reader.Value);
if (eTypeVal == null) return Enum.Parse(typeof(eType), (string)reader.Value);
return Enum.Parse(typeof(eType), eTypeVal.Name);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Here is my version of an enum converter for ANY enum type... it will handle either a numeric value or a string value for the incoming value. As well as nullable vs non-nullable results.
public class MyEnumConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
if (!objectType.IsEnum)
{
var underlyingType = Nullable.GetUnderlyingType(objectType);
if (underlyingType != null && underlyingType.IsEnum)
objectType = underlyingType;
}
return objectType.IsEnum;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (!objectType.IsEnum)
{
var underlyingType = Nullable.GetUnderlyingType(objectType);
if (underlyingType != null && underlyingType.IsEnum)
objectType = underlyingType;
}
var value = reader.Value;
string strValue;
if (value == null || string.IsNullOrWhiteSpace(value.ToString()))
{
if (existingValue == null || Nullable.GetUnderlyingType(existingValue.GetType()) != null)
return null;
strValue = "0";
}
else
strValue = value.ToString();
int intValue;
if (int.TryParse(strValue, out intValue))
return Enum.ToObject(objectType, intValue);
return Enum.Parse(objectType, strValue);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}