I want to serialize and deserialize Nullable DateTime to/from JSON but I do not want to annotate it with JsonConverterAttribute. However, I would like to keep it at once place in JsonSerializerSettings not bloating DTOs with those attributes keeping DTOs clean as usual.
Here is DTO:
public class Post
{
public DateTime? Created { get; set; }
}
Here is Custom JsonConverter:
internal class EpochDateTimeConverter : Newtonsoft.Json.JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(DateTime).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var t = (long)Convert.ToDouble(reader.Value.ToString());
return t.FromUnixTime();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
long ticks;
DateTime valueAsDate = (DateTime)value;
if (valueAsDate != DateTime.MinValue)
{
if (value is DateTime)
{
var epoc = new DateTime(1970, 1, 1);
var delta = (valueAsDate) - epoc;
if (delta.TotalSeconds < 0)
{
throw new ArgumentOutOfRangeException("Unix epoc starts January 1st, 1970");
}
ticks = (long)delta.TotalSeconds;
}
else
{
throw new Exception("Expected date object value.");
}
writer.WriteValue(ticks);
}
}
}
Here is the minimal repro:
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
namespace NameSpaceSample
{
public class Post
{
public DateTime? Created { get; set; }
}
public class Program
{
public static void Main(string[] args)
{
var settings = new JsonSerializerSettings()
{
NullValueHandling = NullValueHandling.Ignore,
Converters = new List<JsonConverter>
{
new EpochDateTimeConverter()
}
};
string postAsJson = JsonConvert.SerializeObject(new Post { Created = DateTime.UtcNow }, settings);
Console.WriteLine(postAsJson);// {"Created":"2015-09-17T17:15:06.6160689Z"}
var json = "{\"Created\":1442510191}";
Post post = JsonConvert.DeserializeObject<Post>(json, settings);//Exception here
Console.ReadKey();
}
}
}
The exception thrown at that line is:
JsonReaderException:
Error reading date. Unexpected token: Integer. Path 'Created', line 1, position 21.
NOTE:
I know this can be resolved by just annotating it with JsonConverterAttribute as below but I don't want to do that for aforementioned reason.
public class Post
{
[JsonConverter(typeof(EpochDateTimeConverter))]
public DateTime? Created { get; set; }
}
Figured it out on my own. I just had to change CanConvert function to following:
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTime) || objectType == typeof(DateTime?);
}
It is not very easy to find out. Putting my answer here to help others if they ever face this.
I give you my way from any object to another object so that you don't worry transfer anything ,thanks
public static T ConvertTo<T>(this object value)
{
T returnValue = default(T);
if (value is T)
{
returnValue = (T)value;
}
else
{
try
{
returnValue = (T)Convert.ChangeType(value, typeof(T));
}
catch (InvalidCastException)
{
returnValue = default(T);
}
}
return returnValue;
}
The CanConvert(Type objectType) method of the JsonConverter determines if that converter will be used for the current property that is being serialized/deserialized.
As the type of your property is DateTime? and that is not assignable from DateTime it returns false and the converter is then not being used.
You just need to change the method to the following:
public override bool CanConvert(Type objectType)
{
return typeof(DateTime?).IsAssignableFrom(objectType);
}
Related
I have a converter like this
class MultiFormatDateConverter : JsonConverter
{
public List<string> DateTimeFormats { get; set; }
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTime);
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
{
string dateString = (string)reader.Value;
DateTime date;
foreach (string format in DateTimeFormats)
{
// adjust this as necessary to fit your needs
if (DateTime.TryParseExact(dateString, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out date))
return date;
}
throw new System.Text.Json.JsonException("Unable to parse \"" + dateString + "\" as a date.");
}
}
and here is the configuration
var settings = new JsonSerializerSettings();
settings.DateParseHandling = DateParseHandling.None;
settings.Converters.Add(new MultiFormatDateConverter
{
DateTimeFormats = new List<string> { "yyyyMMddTHHmmssZ", "yyyy-MM-ddTHH:mm","MMMM yyyy","dd/MM/yyyy","dd/MM/yy","MMM-yy","MMM yy"
}
});
and here is how I am calling it:
List<KipReport> rpt730 = JsonConvert.DeserializeObject<List<KipReport>>(responseBody, settings);
This is the JSON and class
[
{
"Name":"Alex",
"MonthWorked":"January 2021",
"LastEdtDate":"16/02/2021",
"LastEditBy":"san"
}
]
class KipReport
{
public string Name { get; set; }
public DateTime? MonthWorked { get; set; }
public DateTime? LastEditDate { get; set; }
}
Mine is a web API and here is the controller which calls the function. Please note it calls the function as Task.Run()
[HttpGet]
public async Task<IActionResult> Get()
{
await Task.Run(()=>_kReport.GetKReports());
return Accepted();
}
When executing it says
16/03/2021 is not a valid date format
Then I used this way for converting than a converter
var settings = new IsoDateTimeConverter { DateTimeFormat = "dd/MM/yyyy" };
Then error is with January 2021 is not a valid date
Does it means, it's not considering the converter??
Since I have a different format for dates I am using a converter.
So for Web API/Task.Run do we need to do anything specific for the Custom converter?
Your properties are of type DateTime? (i.e. nullable value types) so in CanConvert you must check for objectType == typeof(DateTime?) as well as objectType == typeof(DateTime). Then, in Read(), if the incoming objectType is typeof(DateTime?) you should return null in the event of a null JSON token.
The following fixed converter does this and also skips comments:
class MultiFormatDateConverter : JsonConverter
{
public List<string> DateTimeFormats { get; set; } = new ();
public override bool CanConvert(Type objectType) =>
objectType == typeof(DateTime) || objectType == typeof(DateTime?);
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) => throw new NotImplementedException();
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
{
if (reader.MoveToContent().TokenType == JsonToken.Null)
return objectType == typeof(DateTime?) ? null : throw new System.Text.Json.JsonException("Unable to parse null as a date.");
else if (reader.TokenType != JsonToken.String)
throw new System.Text.Json.JsonException("Unable to parse token \"" + reader.TokenType + "\" as a date.");
string dateString = (string)reader.Value;
foreach (string format in DateTimeFormats)
{
// adjust this as necessary to fit your needs
if (DateTime.TryParseExact(dateString, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date))
return date;
}
throw new System.Text.Json.JsonException("Unable to parse \"" + dateString + "\" as a date.");
}
}
public static partial class JsonExtensions
{
public static JsonReader MoveToContent(this JsonReader reader)
{
while ((reader.TokenType == JsonToken.Comment || reader.TokenType == JsonToken.None) && reader.Read())
;
return reader;
}
}
Notes:
In your JSON you have a property named "LastEdtDate" while the corresponding c# property is LastEditDate. The JSON property name is missing the letter i in Edit and so will not get bound to the c# property. I assume this is a typo in the question, but if not, you will need to add [JsonProperty("LastEdtDate")] to LastEditDate.
Demo fiddle here.
Consider
public class InQuestion<TType>
{
// JsonConverter(typeof CustomConverter))
public Task<TType> toConvert { get; set; }
}
How can I (de)serialize this class with json.net?
What I think I actually want to serialize is the underlying Task< T >.Result, which can then be deserialized with Task< T >.FromResult(). If I am to use custom JsonConverter, I cannot pass generic TType through Attribute, to reconstruct (or retrieve) TType object in the JsonConverter. Hence I'm stuck.
Question came to be from this code:
public class Program
{
public class InQuestion<TType>
{
public Task<TType> toConvert { get; set; }
}
public class Result
{
public int value { get; set; }
}
public static async Task Main()
{
var questionable = new InQuestion<Result>();
questionable.toConvert = Task.Run(async () => new Result { value = 42 });
await questionable.toConvert;
string json = JsonConvert.SerializeObject(questionable);
Debug.WriteLine(json);
InQuestion<Result> back = JsonConvert.DeserializeObject(json, typeof(InQuestion<Result>)) as InQuestion<Result>;
Debug.Assert(back?.toConvert?.Result?.value == 42);
}
}
which, surprisingly to me, halts, during the call to JsonConvert.DeserializeObject. https://github.com/JamesNK/Newtonsoft.Json/issues/1886 talks about the issue and recommends reasonable "Don't ever serialize/deserialize task.", but doesn't actually advice how to serialize the underlying Task< T >.Result.
A Task is a promise of a future value, and of course you cannot serialize a value that has yet to be provided.
Because the InQuestion object holds a Task member, you cannot serialize and deserialize the InQuestion object.
The workaround is to serialize the result, and reconstruct the InQuestion object after deserialization.
public static async Task Main()
{
var questionable = new InQuestion<Result>();
questionable.toConvert = Task.Run(async () => new Result { value = 42 });
Result result = await questionable.toConvert;
string json = JsonConvert.SerializeObject(result);
Result back = JsonConvert.DeserializeObject(json, typeof<Result>) as Result;
InQuestion<Result> reconstructed = new InQuestion<Result>()
{
toConvert = Task.FromResult(back)
};
}
I have found two solutions to this problem.
From the Add support for generic JsonConverter instantiation:
[JsonConverter(typeof(InQuestionConverter<>))]
public class InQuestion<TResult>
{
public Task<TResult> toConvert { get; set; }
}
public class Result
{
public int value { get; set; }
public string text { get; set; }
public override bool Equals(object obj)
{
return obj is Result result &&
value == result.value &&
text == result.text;
}
}
public class InQuestionConverter<TResult> : JsonConverter<InQuestion<TResult>>
{
public override InQuestion<TResult> ReadJson(JsonReader reader, Type objectType, InQuestion<TResult> existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (hasExistingValue)
existingValue.toConvert = Task.FromResult(serializer.Deserialize<TResult>(reader));
else
existingValue = new InQuestion<TResult>
{
toConvert = Task.FromResult(serializer.Deserialize<TResult>(reader))
};
return existingValue;
}
public override void WriteJson(JsonWriter writer, InQuestion<TResult> value, JsonSerializer serializer)
{
serializer.Serialize(writer, value.toConvert.Result, typeof(TResult));
}
}
public sealed class CustomContractResolver : DefaultContractResolver
{
protected override JsonConverter ResolveContractConverter(Type objectType)
{
var typeInfo = objectType.GetTypeInfo();
if (typeInfo.IsGenericType && !typeInfo.IsGenericTypeDefinition)
{
var jsonConverterAttribute = typeInfo.GetCustomAttribute<JsonConverterAttribute>();
if (jsonConverterAttribute != null && jsonConverterAttribute.ConverterType.GetTypeInfo().IsGenericTypeDefinition)
{
Type t = jsonConverterAttribute.ConverterType.MakeGenericType(typeInfo.GenericTypeArguments);
object[] parameters = jsonConverterAttribute.ConverterParameters;
return (JsonConverter)Activator.CreateInstance(t, parameters);
}
}
return base.ResolveContractConverter(objectType);
}
}
public static void Main()
{
var questionable = new InQuestion<Result>();
questionable.toConvert = Task.Run(async () => { return new Result { value = 42, text = "fox" }; });
questionable.toConvert.Wait();
string json = JsonConvert.SerializeObject(questionable, Formatting.None, new JsonSerializerSettings { ContractResolver = new CustomContractResolver() });
InQuestion<Result> back = JsonConvert.DeserializeObject(json, typeof(InQuestion<Result>), new JsonSerializerSettings { ContractResolver = new CustomContractResolver() }) as InQuestion<Result>;
Debug.Assert(back.toConvert.Result.Equals(questionable.toConvert.Result));
return;
}
Enables a custom ContractResolver which will point to correct generic instantiation of JsonConverter<TResult>, in which serialization is straightforward. This requires configuring JsonSerializerSettings and providing serialization for the entire InQuestion class (note that converter doesn't check for Task.IsCompleted in this sample).
Alternatively, using JsonConverterAttribute just on properties of type Task<T> and relying on reflection to retrieve TResult type from non-generic Converter:
public class InQuestion<TResult>
{
[JsonConverter(typeof(FromTaskOfTConverter))]
public Task<TResult> toConvert { get; set; }
}
public class FromTaskOfTConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return IsDerivedFromTaskOfT(objectType);
}
static bool IsDerivedFromTaskOfT(Type type)
{
while (type.BaseType != typeof(object))
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>))
return true;
type = type.BaseType;
}
return false;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
Debug.Assert(IsDerivedFromTaskOfT(objectType));
Type TResult = objectType.GetGenericArguments()[0];
object ResultValue = serializer.Deserialize(reader, TResult);
return typeof(Task).GetMethod("FromResult").MakeGenericMethod(TResult).Invoke(null, new[] { ResultValue });
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
Type objectType = value.GetType();
Debug.Assert(IsDerivedFromTaskOfT(objectType));
Type TResult = objectType.GetGenericArguments()[0];
Type TaskOfTResult = typeof(Task<>).MakeGenericType(TResult);
if ((bool)TaskOfTResult.GetProperty("IsCompleted").GetValue(value) == true)
{
object ResultValue = TaskOfTResult.GetProperty("Result").GetValue(value);
serializer.Serialize(writer, ResultValue, TResult);
}
else
{
serializer.Serialize(writer, Activator.CreateInstance(TResult));
}
}
}
public static void Main()
{
var questionable = new InQuestion<Result>();
questionable.toConvert = Task.Run(async () => { return new Result { value = 42, text = "fox" }; });
questionable.toConvert.Wait();
string json = JsonConvert.SerializeObject(questionable);
InQuestion<Result> back = JsonConvert.DeserializeObject(json, typeof(InQuestion<Result>)) as InQuestion<Result>;
Debug.Assert(back.toConvert.Result.Equals(questionable.toConvert.Result));
return;
}
With all that, I won't mark this accepted, since I lack understanding in both generics reflection and json.net.
I'd like DateTime fields that are set to DateTime.MinValue returned by my Web API to be serialized to NULL instead of "0001-01-01T00:00:00".
I understand there's a way to get JSON.NET to omit fields that are set to default values, but I would prefer JSON.NET to specifically serialize DateTime MinValue / "0001-01-01T00:00:00" as null.
Is there a way to do this?
Create a custom converter which serializes DateTime.MinValue into null, and (if required) deserializes null into DateTime.MinValue:
public class MinDateTimeConverter : DateTimeConverterBase
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.Value == null)
return DateTime.MinValue;
return (DateTime)reader.Value;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
DateTime dateTimeValue = (DateTime)value;
if (dateTimeValue == DateTime.MinValue)
{
writer.WriteNull();
return;
}
writer.WriteValue(value);
}
}
You can then use attributes to add the converter to your data class, as shown in this example:
public class Example
{
[JsonConverter(typeof(MinDateTimeConverter))]
public DateTime ValueOne { get; set; }
[JsonConverter(typeof(MinDateTimeConverter))]
public DateTime ValueTwo { get; set; }
}
public static void Main(string[] args)
{
Example data = new Example();
data.ValueOne = DateTime.MinValue;
data.ValueTwo = DateTime.Now;
JsonSerializer serializer = new JsonSerializer();
using (StringWriter writer = new StringWriter())
{
serializer.Serialize(writer, data);
Console.Write(writer.ToString());
}
Console.ReadKey();
}
Console output:
{"ValueOne":null,"ValueTwo":"2016-10-26T09:54:48.497463+01:00"}
Custom DateTime Json Converter
public class DateTimeConverter : JsonConverter
{
public override void WriteJson(JsonWriter jsonWriter, object inputObject,JsonSerializer jsonSerializer)
{
// Typecast the input object
var dateTimeObject = inputObject as DateTime?;
// Set the properties of the Json Writer
jsonWriter.Formatting = Newtonsoft.Json.Formatting.Indented;
if(dateTimeObject == DateTime.MinValue)
jsonWriter.WriteValue((DateTime?)null);
else
jsonWriter.WriteValue(dateTimeObject);
}
public override object ReadJson(JsonReader reader,
Type objectType,
object existingValue,
JsonSerializer serializer)
{
DateTime? readValue = reader.ReadAsDateTime();
return (readValue == null) ? DateTime.MinValue : readValue;
}
public override bool CanConvert(Type objectType)
{
return typeof(DateTime?).IsAssignableFrom(objectType);
}
}
Usage:
public class DateTest
{
[JsonConverterAttribute(typeof(DateTimeConverter))]
public DateTime? MyDateTime { get; set;}
[JsonConverterAttribute(typeof(DateTimeConverter))]
public DateTime? MyDateTime1 { get; set; }
}
void Main()
{
DateTest dateTest = new DateTest
{
MyDateTime = DateTime.MinValue,
MyDateTime1 = DateTime.MaxValue
};
Console.WriteLine(JsonConvert.SerializeObject(dateTest));
}
Result:
{
"MyDateTime": null,
"MyDateTime1": "9999-12-31T23:59:59.9999999"
}
Not 100% clear on why the original poster wanted this and it's 5 years down the line but I too wanted to achieve this.
However, in my case the issue was that when I serialised my object it was inserting the DateTime fields because they hadn't been specified on the incoming object but were being returned as MinValue not null.
Thus when I deserialised at the other end these fields were showing as 'dirty' and being parsed.
In this case you can just amend your JsonSerializerSettings block to include 'ignore' in the default value handling:
string serialisedMessage = JsonConvert.SerializeObject(message, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore
});
So I'm leaving this in case of future users who are in my position.
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 created a test console application that has a simple class named Other. As an exercise, I want to set the OtherString property to null when it is serialized. I know how I could do this with a custom ContractResolver. I need to be able to do this with a custom Converter too.
The first converter I wrote was simple and the way I thought it should be. However, it would throw a "Self referencing loop detected with type 'JsonContractandConvert.Models.Other'. Path ''." exception. After doing some reading, I made some changes and now I have a working converter. These changes are a lot more verbose, but work.
My question is why are these changes required and is there a better way to do this with a Converter?
Other Class:
[JsonConverter(typeof(OtherConverter))]
public class Other
{
public int Id { get; set; }
public string OtherString { get; set; }
public int OtherInt { get; set; }
public string OtherName
{
get
{
return "Other Name = " + this.OtherString;
}
}
}
First Attempt: (This one throws an exception)
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (!this.CanConvert(value.GetType())) return;
var entity = value as Other;
if (entity == null) return;
entity.OtherString = null;
serializer.Serialize(writer, entity);
}
Second Attempt: (This works as expected)
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (!this.CanConvert(value.GetType())) return;
var entity = value as Other;
if (entity == null) return;
entity.OtherString = null;
writer.WriteStartObject();
var props = entity.GetType().GetProperties();
foreach (var propertyInfo in props)
{
var ignorAttribute =
propertyInfo.CustomAttributes.FirstOrDefault(i => i.AttributeType == typeof(JsonIgnoreAttribute));
if (ignorAttribute != null) continue;
var tempVal = propertyInfo.GetValue(entity);
if (tempVal == null) continue;
writer.WritePropertyName(propertyInfo.Name);
serializer.Serialize(writer, tempVal);
}
}
writer.WriteEndObject();
}
Edit:
Here's the code from the console app that I'm using to test.
class Program
{
static void Main(string[] args)
{
var otherObj = new Other { Id = 123, OtherInt = 456, OtherString = "This is the other string"};
var json = JsonConvert.SerializeObject(otherObj, Formatting.Indented);
Console.WriteLine(json);
}
}
Answering my own questions... (I think I've finally got my head around Json Converters)
To answer my basic question as to why one way works and the other doesn't. I think the answer is just that the way that doesn't work is just wrong. From what I can tell, you need to use the writer object. If you don't it won't work. (There may be scenarios where this is not the case, but I never found one.)
If you merely want to remove a property from the Json, use [JsonIgnore]. (Simple right?)
If you want to change the value some how, you'll need to use a Converter. By using a Converter, you're taking responsibility for serializing what ever object the Converter handles. So if the object is a simple type like a String, or an Int it's really easy. If it's something more complex like an array or complex object, then it takes a little more planning.
Here's an example of a simple String Converter:
Class to serialize:
public class Account
{
public int Id { get; set; }
public string AccountName { get; set; }
[JsonIgnore]
public virtual Account DefaultAssignTo { get; set; }
public int? DefaultAssignToId { get; set; }
[JsonIgnore]
public virtual ICollection<Role> Roles { get; set; }
[JsonIgnore]
public virtual Other Other { get; set; }
public int? OtherId { get; set; }
[JsonConverter(typeof(StringConverter))]
public string OtherName
{
get
{
return "Name = " + this.AccountName;
}
}
}
Converter:
public class StringConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(string).IsAssignableFrom(objectType);
}
public override bool CanRead
{
get { return false; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (!this.CanConvert(value.GetType())) return;
writer.WriteValue("blah blah blah");
}
}
Run: (As you can see I'm using Entity Framework for my data)
using (var db = new Context())
{
var json = JsonConvert.SerializeObject(db.Accounts.FirstOrDefault(), Formatting.Indented,
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
Console.WriteLine(json);
}
Returns:
{
"Id": 43,
"AccountName": "John",
"DefaultAssignToId": 43,
"OtherId": 19,
"OtherName": "blah blah blah"
}
One of the things that messed me up for a bit was when to use writer.WriteStartObject(). Basically if you are Converting a complex object, you need to use it. If you do, then you need to create all of the property names and values. In the OP you can see an example of how I did this. The biggest down side is any Json attributes that properties are decorated with don't automatically happen. So if you can, decorate the class and let the serializer deal with it. Here's an interesting problem you can run into if you use it wrong.
In the String converter replace:
writer.WriteValue("blah blah blah");
with:
writer.WriteStartObject();
writer.WritePropertyName("BlahProp");
serializer.Serialize(writer, "blah blah blah");
writer.WriteEndObject();
run it again and here's the output: (Notice how OtherName is an object now instead of a string)
{
"Id": 43,
"AccountName": "John",
"DefaultAssignToId": 43,
"OtherId": 19,
"OtherName": {
"BlahProp": "blah blah blah"
}
}
The Roles collection also needed a Converter. Two things to note are first, you don't need to specify the property name and second, any Json attributes that the Role class is decorated with will work as expected.
using BaseCollection = System.Collections.Generic.ICollection<JsonContractandConvert.Models.Role>;
public class RemoveAccountsFromRolesConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(BaseCollection).IsAssignableFrom(objectType);
}
public override bool CanRead
{
get { return false; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (!this.CanConvert(value.GetType())) return;
var entities = value as BaseCollection;
if (entities == null) return;
writer.WriteStartArray();
foreach (var entity in entities)
{
entity.Accounts = null;
serializer.Serialize(writer, entity);
}
writer.WriteEndArray();
}
}
Lastly, here's a couple of converters I created for my real project:
This one converts an object to a shallow copy. This is useful if you have a collection where what that collection contains could cause a self referencing loop.
public class ShallowCopyCollectionConverter<TCollectionType, TCopyType> : JsonConverter
where TCollectionType : IEnumerable<TbdEntity>
where TCopyType : TbdEntity, new()
{
public override bool CanConvert(Type objectType)
{
return typeof(TCollectionType).IsAssignableFrom(objectType);
}
public override bool CanRead
{
get { return false; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (this.CanConvert(value.GetType()) == false) return;
var entities = (TCollectionType)value;
writer.WriteStartArray();
foreach (var entity in entities)
{
serializer.Serialize(writer, entity.ShallowCopy<TCopyType>()); //ShallowCopy<> is a method in the base class that all of my classes extend.
}
writer.WriteEndArray();
}
}
This one will convert an abstract using the concrete type:
public class DataSnapInConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(DataSnapIn) == (objectType);
}
public override bool CanWrite
{
get { return false; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (this.CanConvert(objectType) == false) return null;
var jo = JObject.Load(reader);
var typeName = jo["snapInType"] ?? jo["SnapInType"]; //the abstract classes have this property to identify what concrete class they are.
var typeNameString = typeName.ToString();
var deserializeType = Type.GetType(typeNameString);
if(deserializeType == null)
throw new Exception("SnapInType is null or does not reference a valid class.");
var result = Activator.CreateInstance(deserializeType);
serializer.Populate(jo.CreateReader(), result);
return result;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}