应用于类的JsonConverter导致应用于属性的JsonConverter被忽略

时间:2018-01-22 17:25:47

标签: c# json.net

我定义了两个JsonConverter类。一个我附在课堂上,另一个我附在那个班级的财产上。如果我只将转换器附加到属性,它可以正常工作。只要我将一个单独的转换器附加到该类,它就会忽略附加到该属性的转换器。 如何让它不跳过这样的JsonConverterAttributes?

这是类级转换器(我改编自:Alternate property name while deserializing)。我将它附加到测试类,如下:

[JsonConverter(typeof(FuzzyMatchingJsonConverter<JsonTestData>))]

然后这是FuzzyMatchingJsonConverter本身:

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();
        }
    }
}

不要陷入DictionaryToObjectMapper(它是专有的,但使用模糊匹配逻辑来处理拼写变化)。这是下一个JsonConverter,它会将"Y""Yes""T""True"等更改为布尔值。我从这个来源改编了它: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
    }
}

以下是我创建单元测试中使用的测试类的方法:

[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);
    }
}

1 个答案:

答案 0 :(得分:1)

应用于您的属性的[JsonConverter(typeof(BooleanJsonConverter))]不起作用的原因是您为包含类型提供了JsonConverter,并且没有为其中的成员调用应用的转换器ReadJson()方法。

当转换器应用于某个类型时,在(反)序列化之前,Json.NET使用反射为类型创建JsonContract,该类型指定如何映射类型和JSON。对于具有属性的对象,生成JsonObjectContract,其中包括构造和填充类型的方法,并列出要序列化的类型的所有成员,包括其名称和任何应用的转换器。构建合同后,方法JsonSerializerInternalReader.PopulateObject()使用它来实际反序列化对象。

当转换器 应用于某个类型时,将跳过上述所有逻辑。相反,JsonConverter.ReadJson()必须执行所有操作,包括反序列化和设置所有成员值。如果这些成员碰巧应用了转换器,ReadJson()将需要注意这一事实并手动调用转换器。这就是你的转换器需要在这里做的事情:

        foreach (JProperty jp in jo.Properties())
        {
            var prop = Mapper.KeyToProperty[jp.Name];
            // Check for and use [JsonConverter(typeof(...))] if applied to the member.
            prop?.SetValue(instance, jp.Value.ToObject(prop.PropertyType, serializer));
        }

那么,怎么做?一种方法是使用c#反射工具来检查适当的属性。幸运的是,Json.NET已经为您构建了JsonObjectContract;您只需致电:{/ p>即可在ReadJson()内访问该帐户

var contract = serializer.ContractResolver.ResolveContract(objectType) as JsonObjectContract;

完成后,您可以使用已构建的合同来指导反序列化。

由于您没有提供FuzzyMatchingJsonConverter的实际示例,因此我创建了类似的内容,以便将蛇案例和pascal案例属性反序列化为具有驼峰案例命名的对象:

public abstract class FuzzyMatchingJsonConverterBase : JsonConverter
{
    protected abstract JsonProperty FindProperty(JsonObjectContract contract, string propertyName);

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var contract = serializer.ContractResolver.ResolveContract(objectType) as JsonObjectContract;
        if (contract == null)
            throw new JsonSerializationException(string.Format("Contract for type {0} is not a JsonObjectContract", objectType));

        if (reader.TokenType == JsonToken.Null)
            return null;

        if (reader.TokenType != JsonToken.StartObject)
            throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));

        existingValue = existingValue ?? contract.DefaultCreator();

        while (reader.Read())
        {
            switch (reader.TokenType)
            {
                case JsonToken.Comment:
                    break;
                case JsonToken.PropertyName:
                    {
                        var propertyName = (string)reader.Value;
                        reader.ReadAndAssert();
                        var jsonProperty = FindProperty(contract, propertyName);
                        if (jsonProperty == null)
                            continue;
                        object itemValue;
                        if (jsonProperty.Converter != null && jsonProperty.Converter.CanRead)
                        {
                            itemValue = jsonProperty.Converter.ReadJson(reader, jsonProperty.PropertyType, jsonProperty.ValueProvider.GetValue(existingValue), serializer);
                        }
                        else
                        {
                            itemValue = serializer.Deserialize(reader, jsonProperty.PropertyType);
                        }
                        jsonProperty.ValueProvider.SetValue(existingValue, itemValue);
                    }
                    break;
                case JsonToken.EndObject:
                    return existingValue;
                default:
                    throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
            }
        }
        throw new JsonReaderException("Unexpected EOF!");
    }

    public override bool CanWrite { get { return false; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

public abstract class FuzzySnakeCaseMatchingJsonConverterBase : FuzzyMatchingJsonConverterBase
{
    protected override JsonProperty FindProperty(JsonObjectContract contract, string propertyName)
    {
        // Remove snake-case underscore.
        propertyName = propertyName.Replace("_", "");
        // And do a case-insensitive match.
        return contract.Properties.GetClosestMatchProperty(propertyName);
    }
}

// This type should be applied via attributes.
public class FuzzySnakeCaseMatchingJsonConverter : FuzzySnakeCaseMatchingJsonConverterBase
{
    public override bool CanConvert(Type objectType)
    {
        throw new NotImplementedException();
    }
}

// This type can be used in JsonSerializerSettings.Converters
public class GlobalFuzzySnakeCaseMatchingJsonConverter : FuzzySnakeCaseMatchingJsonConverterBase
{
    readonly IContractResolver contractResolver;

    public GlobalFuzzySnakeCaseMatchingJsonConverter(IContractResolver contractResolver)
    {
        this.contractResolver = contractResolver;
    }

    public override bool CanConvert(Type objectType)
    {
        if (objectType.IsPrimitive || objectType == typeof(string))
            return false;
        var contract = contractResolver.ResolveContract(objectType);
        return contract is JsonObjectContract;
    }
}

public static class JsonReaderExtensions
{
    public static void ReadAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (!reader.Read())
            throw new JsonReaderException("Unexpected EOF!");
    }
}

然后你会按如下方式应用它:

[JsonConverter(typeof(FuzzySnakeCaseMatchingJsonConverter))]
public class 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; }
}

注意:

  • 我避免将JSON预加载到中间JToken层次结构中,因为没有必要这样做。

  • 您没有提供自己的转换器的工作示例,因此我无法在此答案中为您修复此问题,但您希望将其从FuzzyMatchingJsonConverterBase继承并且然后写下你protected abstract JsonProperty FindProperty(JsonObjectContract contract, string propertyName);的版本。

  • 您可能还需要检查并使用JsonProperty的其他属性,例如JsonProperty.ItemConverterJsonProperty.IgnoredJsonProperty.ShouldDeserialize等。但如果你这样做,你最终可能会重复JsonSerializerInternalReader.PopulateObject()的整个逻辑。

  • 应在null的开头附近检查ReadJson() JSON值。

示例工作.Net fiddle,表明可以成功反序列化以下JSON,从而调用类型和成员转换器:

{
  "test_id": "hello",
  "minimum_distance": 10101.1,
  "tax_included": "yes",
  "is_mets_fan": "no"
}