I defined two JsonConverter classes. One I attach to the class, the other I attach to a property of that class. If I only attach the converter to the property, it works fine. As soon as I attach a separate converter to the class, it ignores the one attached to the property. How can I make it not skip such JsonConverterAttributes?
Here is the class-level converter (which I adapted from this: Alternate property name while deserializing). I attach it to the test class like so:
[JsonConverter(typeof(FuzzyMatchingJsonConverter<JsonTestData>))]
Then here is the FuzzyMatchingJsonConverter itself:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Optimizer.models;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Optimizer.Serialization
{
/// <summary>
/// Permit the property names in the Json to be deserialized to have spelling variations and not exactly match the
/// property name in the object. Thus puntuation, capitalization and whitespace differences can be ignored.
///
/// NOTE: As implemented, this can only deserialize objects from a string, not serialize from objects to a string.
/// </summary>
/// <seealso cref="https://stackoverflow.com/questions/19792274/alternate-property-name-while-deserializing"/>
public class FuzzyMatchingJsonConverter<T> : JsonConverter
{
/// <summary>
/// Map the json property names to the object properties.
/// </summary>
private static DictionaryToObjectMapper<T> Mapper { get; set; } = null;
private static object SyncToken { get; set; } = new object();
static void InitMapper(IEnumerable<string> jsonPropertyNames)
{
if (Mapper == null)
lock(SyncToken)
{
if (Mapper == null)
{
Mapper = new DictionaryToObjectMapper<T>(
jsonPropertyNames,
EnumHelper.StandardAbbreviations,
ModelBase.ACCEPTABLE_RELATIVE_EDIT_DISTANCE,
ModelBase.ABBREVIATION_SCORE
);
}
}
else
{
lock(SyncToken)
{
// Incremental mapping of additional attributes not seen the first time for the second and subsequent objects.
// (Some records may have more attributes than others.)
foreach (var jsonPropertyName in jsonPropertyNames)
{
if (!Mapper.CanMatchKeyToProperty(jsonPropertyName))
throw new MatchingAttributeNotFoundException(jsonPropertyName, typeof(T).Name);
}
}
}
}
public override bool CanConvert(Type objectType) => objectType.IsClass;
/// <summary>
/// If false, this class cannot serialize (write) objects.
/// </summary>
public override bool CanWrite { get => false; }
/// <summary>
/// Call the default constructor for the object and then set all its properties,
/// matching the json property names to the object attribute names.
/// </summary>
/// <param name="reader"></param>
/// <param name="objectType">This should match the type parameter T.</param>
/// <param name="existingValue"></param>
/// <param name="serializer"></param>
/// <returns>The deserialized object of type T.</returns>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// Note: This assumes that there is a default (parameter-less) constructor and not a constructor tagged with the JsonCOnstructorAttribute.
// It would be better if it supported those cases.
object instance = objectType.GetConstructor(Type.EmptyTypes).Invoke(null);
JObject jo = JObject.Load(reader);
InitMapper(jo.Properties().Select(jp => jp.Name));
foreach (JProperty jp in jo.Properties())
{
var prop = Mapper.KeyToProperty[jp.Name];
prop?.SetValue(instance, jp.Value.ToObject(prop.PropertyType, serializer));
}
return instance;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
}
Do not get bogged down with DictionaryToObjectMapper (it is proprietary, but uses fuzzy matching logic to deal with spelling variations). Here is the next JsonConverter, that will change "Y", "Yes", "T", "True", etc into Boolean true values. I adapted it from this source: https://gist.github.com/randyburden/5924981
using System;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Optimizer.Serialization
{
/// <summary>
/// Handles converting JSON string values into a C# boolean data type.
/// </summary>
/// <see cref="https://gist.github.com/randyburden/5924981"/>
public class BooleanJsonConverter : JsonConverter
{
private static readonly string[] Truthy = new[] { "t", "true", "y", "yes", "1" };
private static readonly string[] Falsey = new[] { "f", "false", "n", "no", "0" };
/// <summary>
/// Parse a Boolean from a string where alternative spellings are permitted, such as 1, t, T, true or True for true.
///
/// All values that are not true are considered false, so no parse error will occur.
/// </summary>
public static Func<object, bool> ParseBoolean
= (obj) => { var b = (obj ?? "").ToString().ToLower().Trim(); return Truthy.Any(t => t.Equals(b)); };
public static bool ParseBooleanWithValidation(object obj)
{
var b = (obj ?? "").ToString().ToLower().Trim();
if (Truthy.Any(t => t.Equals(b)))
return true;
if (Falsey.Any(t => t.Equals(b)))
return false;
throw new ArgumentException($"Unable to convert ${obj}into a Boolean attribute.");
}
#region Overrides of JsonConverter
/// <summary>
/// Determines whether this instance can convert the specified object type.
/// </summary>
/// <param name="objectType">Type of the object.</param>
/// <returns>
/// <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
/// </returns>
public override bool CanConvert(Type objectType)
{
// Handle only boolean types.
return objectType == typeof(bool);
}
/// <summary>
/// Reads the JSON representation of the object.
/// </summary>
/// <param name="reader">The <see cref="T:Newtonsoft.Json.JsonReader"/> to read from.</param>
/// <param name="objectType">Type of the object.</param>
/// <param name="existingValue">The existing value of object being read.</param>
/// <param name="serializer">The calling serializer.</param>
/// <returns>
/// The object value.
/// </returns>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
=> ParseBooleanWithValidation(reader.Value);
/// <summary>
/// Specifies that this converter will not participate in writing results.
/// </summary>
public override bool CanWrite { get { return false; } }
/// <summary>
/// Writes the JSON representation of the object.
/// </summary>
/// <param name="writer">The <see cref="T:Newtonsoft.Json.JsonWriter"/> to write to.</param><param name="value">The value.</param><param name="serializer">The calling serializer.</param>
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
//TODO: Implement for serialization
//throw new NotImplementedException("Serialization of Boolean");
// I have no idea if this is correct:
var b = (bool)value;
JToken valueToken;
valueToken = JToken.FromObject(b);
valueToken.WriteTo(writer);
}
#endregion Overrides of JsonConverter
}
}
And here is how I created the test class used in my unit tests:
[JsonConverter(typeof(FuzzyMatchingJsonConverter<JsonTestData>))]
public class JsonTestData: IEquatable<JsonTestData>
{
public string TestId { get; set; }
public double MinimumDistance { get; set; }
[JsonConverter(typeof(BooleanJsonConverter))]
public bool TaxIncluded { get; set; }
[JsonConverter(typeof(BooleanJsonConverter))]
public bool IsMetsFan { get; set; }
[JsonConstructor]
public JsonTestData()
{
TestId = null;
MinimumDistance = double.NaN;
TaxIncluded = false;
IsMetsFan = false;
}
public JsonTestData(string testId, double minimumDistance, bool taxIncluded, bool isMetsFan)
{
TestId = testId;
MinimumDistance = minimumDistance;
TaxIncluded = taxIncluded;
IsMetsFan = isMetsFan;
}
public override bool Equals(object obj) => Equals(obj as JsonTestData);
public bool Equals(JsonTestData other)
{
if (other == null) return false;
return ((TestId ?? "") == other.TestId)
&& (MinimumDistance == other.MinimumDistance)
&& (TaxIncluded == other.TaxIncluded)
&& (IsMetsFan == other.IsMetsFan);
}
public override string ToString() => $"TestId: {TestId}, MinimumDistance: {MinimumDistance}, TaxIncluded: {TaxIncluded}, IsMetsFan: {IsMetsFan}";
public override int GetHashCode()
{
return -1448189120 + EqualityComparer<string>.Default.GetHashCode(TestId);
}
}