OnDeserialized callback with JsonConverter - c#

I´m trying to use the JsonConverter from this answer to Can I specify a path in an attribute to map a property in my class to a child property in my JSON? by Brian Rogers to map nested properties in JSON to a flat object.
The converter works well, but I need to fire the OnDeserialized callback to fill other properties and it´s not fired. If I don´t use the converter, the callback is fired.
Examples:
string json = #"{
'response': {
'code': '000',
'description': 'Response success',
},
'employee': {
'name': 'Test',
'surname': 'Testing',
'work': 'At office'
}
}";
// employee.cs
public class EmployeeStackoverflow
{
[JsonProperty("response.code")]
public string CodeResponse { get; set; }
[JsonProperty("employee.name")]
public string Name { get; set; }
[JsonProperty("employee.surname")]
public string Surname { get; set; }
[JsonProperty("employee.work")]
public string Workplace { get; set; }
[OnDeserialized]
internal void OnDeserializedMethod(StreamingContext context)
{
Workplace = "At Home!!";
}
}
// employeeConverter.cs
public class EmployeeConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
object targetObj = Activator.CreateInstance(objectType);
foreach (PropertyInfo prop in objectType.GetProperties()
.Where(p => p.CanRead && p.CanWrite))
{
JsonPropertyAttribute att = prop.GetCustomAttributes(true)
.OfType<JsonPropertyAttribute>()
.FirstOrDefault();
string jsonPath = (att != null ? att.PropertyName : prop.Name);
JToken token = jo.SelectToken(jsonPath);
if (token != null && token.Type != JTokenType.Null)
{
object value = token.ToObject(prop.PropertyType, serializer);
prop.SetValue(targetObj, value, null);
}
}
return targetObj;
}
public override bool CanConvert(Type objectType)
{
// CanConvert is not called when [JsonConverter] attribute is used
return false;
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
If I add [JsonConverter(typeof(EmployeeConverter))] in the Employee class I obtain:
=== With Converter ===
Code: 000
Name: Test
Surname: Testing
Workplace: At office
If I remove[JsonConverter(typeof(EmployeeConverter))] from the Employee class I obtain:
=== With Converter ===
Code:
Name:
Surname:
Workplace: At Home!!
My goal is to obtain:
=== With Converter ===
Code: 000
Name: Test
Surname: Testing
Workplace: At Home!!
Is the converter missing something?

Once you have created a custom JsonConverter for a type, it is incumbent on the converter to handle everything that needs to be done during deserialization -- including
Calling serialization callbacks.
Skipping ignored properties.
Invoking JsonConverter.ReadJson() for converters attached via attributes to members of the type.
Setting default values, skipping null values, resolving references, etc etc.
The complete logic can be seen in JsonSerializerInternalReader.PopulateObject(), and in theory you might need to make your ReadJson() method duplicate this method. (But in practice you will likely only implement a small, necessary subset of the logic.)
One way to make this task easier is to use Json.NET's own JsonObjectContract type metadata, as returned by JsonSerializer.ContractResolver.ResolveContract(objectType). This information contains the list of serialization callbacks and JsonpropertyAttribute property data used by Json.NET during deserialization. A modified version of the converter that uses this information would be as follows:
// Modified from this answer https://stackoverflow.com/a/33094930
// To https://stackoverflow.com/questions/33088462/can-i-specify-a-path-in-an-attribute-to-map-a-property-in-my-class-to-a-child-pr/
// By https://stackoverflow.com/users/10263/brian-rogers
// By adding handling of deserialization callbacks and some JsonProperty attributes.
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
var contract = serializer.ContractResolver.ResolveContract(objectType) as JsonObjectContract ?? throw new JsonException(string.Format("{0} is not a JSON object", objectType));
var jo = JToken.Load(reader);
if (jo.Type == JTokenType.Null)
return null;
else if (jo.Type != JTokenType.Object)
throw new JsonSerializationException(string.Format("Unexpected token {0}", jo.Type));
var targetObj = contract.DefaultCreator();
// Handle deserialization callbacks
foreach (var callback in contract.OnDeserializingCallbacks)
callback(targetObj, serializer.Context);
foreach (var property in contract.Properties)
{
// Check that property isn't ignored, and can be deserialized.
if (property.Ignored || !property.Writable)
continue;
if (property.ShouldDeserialize != null && !property.ShouldDeserialize(targetObj))
continue;
var jsonPath = property.PropertyName;
var token = jo.SelectToken(jsonPath);
// TODO: default values, skipping nulls, PreserveReferencesHandling, ReferenceLoopHandling, ...
if (token != null && token.Type != JTokenType.Null)
{
object value;
// Call the property's converter if present, otherwise deserialize directly.
if (property.Converter != null && property.Converter.CanRead)
{
using (var subReader = token.CreateReader())
{
if (subReader.TokenType == JsonToken.None)
subReader.Read();
value = property.Converter.ReadJson(subReader, property.PropertyType, property.ValueProvider.GetValue(targetObj), serializer);
}
}
// TODO: property.ItemConverter != null
else
{
value = token.ToObject(property.PropertyType, serializer);
}
property.ValueProvider.SetValue(targetObj, value);
}
}
// Handle deserialization callbacks
foreach (var callback in contract.OnDeserializedCallbacks)
callback(targetObj, serializer.Context);
return targetObj;
}
Demo fiddle here.

Related

How to force JsonConverter.WriteJson() to be called for a null value

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.

Deserialize two values into the same property

I have a client which can call two different versions of a service.
One service only sends a single value:
{
"value" : { ... }
}
The second service always returns multiple values:
{
"values" : [
{ ... },
{ ... }
]
}
Ideally, I'd like to represent this with a single object in my client classes so the user never sees whether it's a single value or multiple values.
public class MyValues
{
public List<Stuff> Values { get; set; }
public Thing Other { get; set; }
}
I think that the only way I'll be able to accomplish this is with a custom JsonConverter class which I apply to MyValues, but I really only want to do something custom when I'm deserializing the property value. I can't seem to figure out if an IContractResolver would be a better way to go (e.g. somehow attach a phantom property to MyValues that deserializes value and puts it into Values.
If I create a custom converter, how to I tell it to deserialize everything else normally (e.g. if Other has an extra properties make sure they are handled appropriately, etc.)
Rather than writing a JsonConverter, you could make a set-only property Value on your MyValues, like so:
public class MyValues
{
[JsonProperty]
Stuff Value
{
set
{
(Values = Values ?? new List<Stuff>(1)).Clear();
Values.Add(value);
}
}
public List<Stuff> Values { get; set; }
public Thing Other { get; set; }
}
It could be public or private if marked with [JsonProperty]. In this case Json.NET will call the Value setter if the singleton "value" property is encountered in the JSON, and call the Values setter if the array "values" property is encountered. Since the property is set-only only the array property will be re-serialized.
To make a custom JsonConverter that has special processing for a few properties of a type but uses default processing for the remainder, you can load the JSON into a JObject, detach and process the custom properties, then populate the remainder from the JObject with JsonSerializer.Populate(), like so:
class MyValuesConverter : CustomPropertyConverterBase<MyValues>
{
protected override void ProcessCustomProperties(JObject obj, MyValues value, JsonSerializer serializer)
{
// Remove the value property for manual deserialization, and deserialize
var jValue = obj.GetValue("value", StringComparison.OrdinalIgnoreCase).RemoveFromLowestPossibleParent();
if (jValue != null)
{
(value.Values = value.Values ?? new List<Stuff>()).Clear();
value.Values.Add(jValue.ToObject<Stuff>(serializer));
}
}
}
public abstract class CustomPropertyConverterBase<T> : JsonConverter where T : class
{
public override bool CanConvert(Type objectType)
{
return typeof(T).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var jObj = JObject.Load(reader);
var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(objectType);
var value = existingValue as T ?? (T)contract.DefaultCreator();
ProcessCustomProperties(jObj, value, serializer);
// Populate the remaining properties.
using (var subReader = jObj.CreateReader())
{
serializer.Populate(subReader, value);
}
return value;
}
protected abstract void ProcessCustomProperties(JObject obj, T value, JsonSerializer serializer);
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
public static class JsonExtensions
{
public static JToken RemoveFromLowestPossibleParent(this JToken node)
{
if (node == null)
return null;
var contained = node.AncestorsAndSelf().Where(t => t.Parent is JContainer && t.Parent.Type != JTokenType.Property).FirstOrDefault();
if (contained != null)
contained.Remove();
// Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
if (node.Parent is JProperty)
((JProperty)node.Parent).Value = null;
return node;
}
}

Convert empty strings to null with Json.Net

I'm having trouble finding a way to automatically deserialize (server side) all EmptyOrWhiteSpace strings to null . Json.Net by default simply assigns the value to the object property, and I need to verify string by string whether it is empty or white space, and then set it to null.
I need this to be done upon deserialization, so I don't have to remember to verify every single string that comes from the client.
How can I override this on Json Net?
After a lot of source digging, I solved my problem.
Turns out all the solutions proposed in the comments only work if I am deserializing a complex object which contains a property that is a string.
In this case, yes, simply modifying the contract resolver works [1].
However, what I needed was a way to convert any string to null upon deserialization, and modifying the contract this way will fail for the case where my object is just a string, i.e.,
public void MyMethod(string jsonSomeInfo)
{
// At this point, jsonSomeInfo is "\"\"",
// an emmpty string.
var deserialized = new JsonSerializer().Deserialize(new StringReader(jsonSomeInfo), typeof(string));
// deserialized = "", event if I used the modified contract resolver [1].
}
What happens is that when we work with a complex object, internally JSON.NET assigns a TokenType of JsonToken.StartObject to the reader, which will cause the deserialization to follow a certain path where property.ValueProvider.SetValue(target, value); is called.
However, if the object is just a string, the TokenType will be JsonToken.String, and the path will be different, and the value provider will never be invoked.
In any event, my solution was to build a custom converter to convert JsonReaders that have TokenType == JsonToken.String (code below).
Solution
public class StringConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(string);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.Value == null) return null;
string text = reader.Value.ToString();
if (string.IsNullOrWhiteSpace(text))
{
return null;
}
return text;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException("Not needed because this converter cannot write json");
}
public override bool CanWrite
{
get { return false; }
}
}
[1] Credits to #Raphaël Althaus.
public class NullToEmptyStringResolver : DefaultContractResolver
{
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
return type.GetProperties()
.Select(p => {
var jp = base.CreateProperty(p, memberSerialization);
jp.ValueProvider = new EmptyToNullStringValueProvider(p);
return jp;
}).ToList();
}
}
public class EmptyToNullStringValueProvider : IValueProvider
{
PropertyInfo _MemberInfo;
public EmptyToNullStringValueProvider(PropertyInfo memberInfo)
{
_MemberInfo = memberInfo;
}
public object GetValue(object target)
{
object result = _MemberInfo.GetValue(target);
if (_MemberInfo.PropertyType == typeof(string) && result != null && string.IsNullOrWhiteSpace(result.ToString()))
{
result = null;
}
return result;
}
public void SetValue(object target, object value)
{
if (_MemberInfo.PropertyType == typeof(string) && value != null && string.IsNullOrWhiteSpace(value.ToString()))
{
value = null;
}
_MemberInfo.SetValue(target, value);
}
}

Strategies for migrating serialized Json.NET document between versions/formats

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.

NullValueHandling.Ignore with JsonConverter::WriteJson

I am trying to perform custom serialisation, all the happy path code works but the null value path is not behaving as I'd like.
I have set the serializer settings to NullValueHandling.Ignore and other parts of my object graph that are null (and don't use my custom serialization) have the null values removed. It looks like the Newtonsoft serializer writes to a string builder so we should be able to 'rewind' any written json tokens but I don't see how to not write anything.
Doing nothing and just returning causes the serializer to throw an exception as the json would be invalid.
Any clues?
public class SpecialConvertor : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value == null || (int)value == 0)
{
if (serializer.NullValueHandling == NullValueHandling.Ignore)
{
//how to make this work?
}
else
{
writer.WriteNull();
}
return;
}
// the rest of WriteJson
}
// the rest of SpecialConvertor
}
NullValueHandling is for object references. In your example, your value is an integer. To omit integer properties with default values, use the setting DefaultValueHandling = DefaultValueHandling.Ignore.
The null check in WriteJson() should not be necessary because Json.NET never calls the converter with a null value. Instead, it writes the name & null value itself -- or not, if NullValueHandling == NullValueHandling.Ignore. So checking for null and rewinding should never be required.
A null value for an object property might still get written when null value handling or default value handling are Ignore if one of your converters writes it explicitly in WriteJson. To prevent that, you can check the settings and skip nulls like so:
public class MyClassConverter : JsonConverter
{
const string Prefix = "My Value Is: ";
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var myClass = (MyClass)value;
writer.WriteStartObject();
if (myClass.StringValue != null
|| (serializer.NullValueHandling != NullValueHandling.Ignore
&& (serializer.DefaultValueHandling & DefaultValueHandling.Ignore) != DefaultValueHandling.Ignore))
{
writer.WritePropertyName("StringValue");
if (myClass.StringValue == null)
writer.WriteNull();
else
serializer.Serialize(writer, Prefix + myClass.StringValue);
}
writer.WriteEndObject();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var s = (string)JValue.Load(reader);
if (s.StartsWith(Prefix))
s = s.Substring(Prefix.Length);
return s;
}
public override bool CanConvert(Type objectType) { return objectType == typeof(MyClass); }
}
[JsonConverter(typeof(MyClassConverter))]
public class MyClass
{
public string StringValue { get; set; }
}

Categories

Resources