NewtonSoft JsonConverter-访问其他属性 [英] NewtonSoft JsonConverter - Access other properties

查看:40
本文介绍了NewtonSoft JsonConverter-访问其他属性的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我需要将十进制的输出json格式化为货币,并在指定要进行序列化的对象指定区域性的情况下,可以嵌套该对象,因此无法在序列化器中预设该选项.我目前的操作方式是使用额外的字符串属性来格式化输出.

[JsonIgnore]
public decimal Cost {get;set;}

[JsonIgnore]
public CultureInfo Culture {get;set;}

public string AsCurrency(decimal value) {
  return string.Format(this.Culture, "{0:c}", value);
}

[JsonProperty("FormattedCost")]
public string FormatedCost {
  get { return this.AsCurrency(this.Cost); }
}

我有很多要处理的属性,我不担心反序列化,另一种语言使用JsonObject填充PDF,所以我想要字符串值.

理想情况下,我想要一个JsonConverter,所以我可以这样做

[JsonProperty("FormattedCost")]
[JsonConverter(typeof(MyCurrencyConverter))]
public decimal Cost {get;set;}

我遇到的问题是如何访问转换器中包含对象的Culture属性.

public class MyCurrencyConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
       var culture = // How do I get the Culture from the parent object?
       writer.WriteValue(string.format(culture, "{0:c}", (decimal)value);

    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof(decimal) == objectType;
    }
}

根据请求的示例JSON.

表示每个Contract类的数组,每个类都有一个Cost和一个Culture.

[{ FormattedCost : "£5000.00"}, { FormattedCost : "$8000.00"}, { FormattedCost : "€599.00"}]

实际对象要复杂得多,具有嵌套资产的多个字段将具有自己的图形.此外,并非所有的小数点都是货币.

我真的不需要为Contract本身编写自定义序列化程序,因为每次属性更改时我都必须对其进行修改.

理想的解决方案是能够使用转换器属性标记某些十进制属性,以便它可以处理它.

我想去的另一种方式是为十进制属性创建一个自定义类,并从十进制进行隐式转换,但是由于某些属性是根据先前结果计算得出的属性,因此变得更加复杂.

WORKAROUND

对于我的用例,我有一个解决方法,但是它使用反射在序列化器中获取私有变量.

var binding = BindingFlags.NonPublic | BindingFlags.Instance;
var writer = serializer.GetType()
                       .GetMethod("GetInternalSerializer", binding)
                       ?.Invoke(serializer, null);
var parent = writer?.GetType()
                   .GetField("_serializeStack", binding)
                   ?.GetValue(writer) is List<object> stack 
                        && stack.Count > 1 ? stack[stack.Count - 2] as MyType: null;

在经过测试的用例中,这为我提供了父对象,但未使用公共API.

解决方案

您想要做的是在对对象的特定属性进行序列化时对其进行拦截和修改,同时对所有其他属性使用默认序列化.可以使用自定义ContractResolver 来代替<应用特定属性时,对该属性的href ="https://www.newtonsoft.com/json/help/html/P_Newtonsoft_Json_Serialization_JsonProperty_ValueProvider.htm" rel ="nofollow noreferrer"> ValueProvider .

首先,定义以下属性和合同解析器:

[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field, AllowMultiple = false)]
public class JsonFormatAttribute : System.Attribute
{
    public JsonFormatAttribute(string formattingString)
    {
        this.FormattingString = formattingString;
    }

    /// <summary>
    /// The format string to pass to string.Format()
    /// </summary>
    public string FormattingString { get; set; }

    /// <summary>
    /// The name of the underlying property that returns the object's culture, or NULL if not applicable.
    /// </summary>
    public string CulturePropertyName { get; set; }
}

public class FormattedPropertyContractResolver : DefaultContractResolver
{
    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        return base.CreateProperties(type, memberSerialization)
            .AddFormatting();
    }
}

public static class JsonContractExtensions
{
    class FormattedValueProvider : IValueProvider
    {
        readonly IValueProvider baseProvider;
        readonly string formatString;
        readonly IValueProvider cultureValueProvider;

        public FormattedValueProvider(IValueProvider baseProvider, string formatString, IValueProvider cultureValueProvider)
        {
            this.baseProvider = baseProvider;
            this.formatString = formatString;
            this.cultureValueProvider = cultureValueProvider;
        }

        #region IValueProvider Members

        public object GetValue(object target)
        {
            var value = baseProvider.GetValue(target);
            var culture = cultureValueProvider == null ? null : (CultureInfo)cultureValueProvider.GetValue(target);
            return string.Format(culture ?? CultureInfo.InvariantCulture, formatString, value);
        }

        public void SetValue(object target, object value)
        {
            // This contract resolver should only be used for serialization, not deserialization, so throw an exception.
            throw new NotImplementedException();
        }

        #endregion
    }

    public static IList<JsonProperty> AddFormatting(this IList<JsonProperty> properties)
    {
        ILookup<string, JsonProperty> lookup = null;

        foreach (var jsonProperty in properties)
        {
            var attr = (JsonFormatAttribute)jsonProperty.AttributeProvider.GetAttributes(typeof(JsonFormatAttribute), false).SingleOrDefault();
            if (attr != null)
            {
                IValueProvider cultureValueProvider = null;
                if (attr.CulturePropertyName != null)
                {
                    if (lookup == null)
                        lookup = properties.ToLookup(p => p.UnderlyingName);
                    var cultureProperty = lookup[attr.CulturePropertyName].FirstOrDefault();
                    if (cultureProperty != null)
                        cultureValueProvider = cultureProperty.ValueProvider;
                }
                jsonProperty.ValueProvider = new FormattedValueProvider(jsonProperty.ValueProvider, attr.FormattingString, cultureValueProvider);
                jsonProperty.PropertyType = typeof(string);
            }
        }
        return properties;
    }
}

接下来,按如下所示定义您的对象:

public class RootObject
{
    [JsonFormat("{0:c}", CulturePropertyName = nameof(Culture))]
    public decimal Cost { get; set; }

    [JsonIgnore]
    public CultureInfo Culture { get; set; }

    public string SomeValue { get; set; }

    public string SomeOtherValue { get; set; }
}

最后,序列化如下:

var settings = new JsonSerializerSettings
{
    ContractResolver = new FormattedPropertyContractResolver
    {
        NamingStrategy = new CamelCaseNamingStrategy(),
    },
};
var json = JsonConvert.SerializeObject(root, Formatting.Indented, settings);

注意:

  1. 由于您没有序列化区域性名称,因此我看不到任何反序列化Cost属性的方法.因此,我从 SetValue 方法中抛出了一个异常. >

    (而且,即使您要序列化区域性名称,由于JSON对象是 无序一组名称/值对,根据如何将元数据添加到合同中的ConfigurableContractResolver描述哪些属性是JSON.Net中的日期.

  2. 您可能想要缓存合同解析器以获得最佳性能.

  3. 另一种方法是将转换器添加到父对象,该转换器通过临时禁用自身,对返回的JObject进行微调,然后将其写出,从而生成对JObject的默认序列化.有关此方法的示例,请参见 JSON.Net在使用[JsonConvert()] .Net小提琴.

    I have a need to format the output json of a decimal to a currency, with the culture specified my the object I am serializing, the object could be nested so I cannot preset the option in the serializer. The current way I am doing this is by using extra string properties that format the output.

    [JsonIgnore]
    public decimal Cost {get;set;}
    
    [JsonIgnore]
    public CultureInfo Culture {get;set;}
    
    public string AsCurrency(decimal value) {
      return string.Format(this.Culture, "{0:c}", value);
    }
    
    [JsonProperty("FormattedCost")]
    public string FormatedCost {
      get { return this.AsCurrency(this.Cost); }
    }
    

    I have alot of properties to deal with, I'm not bothered about Deserializing, the JsonObject is used by a different language to populated a PDF and so I want the string values.

    Ideally I'd like a JsonConverter so I can just do

    [JsonProperty("FormattedCost")]
    [JsonConverter(typeof(MyCurrencyConverter))]
    public decimal Cost {get;set;}
    

    The issue I have is how to access the Culture property of the containing object in the converter.

    public class MyCurrencyConverter : JsonConverter
    {
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
           var culture = // How do I get the Culture from the parent object?
           writer.WriteValue(string.format(culture, "{0:c}", (decimal)value);
    
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    
        public override bool CanConvert(Type objectType)
        {
            return typeof(decimal) == objectType;
        }
    }
    

    As Requested sample JSON.

    for an array of Contract classes that each have a Cost and an Culture.

    [{ FormattedCost : "£5000.00"}, { FormattedCost : "$8000.00"}, { FormattedCost : "€599.00"}]
    

    The actual objects are a lot more complicated, multiple fields with nested Assets that would have their own figures. Additionally not all decimals would be currencies.

    I don't really want to have to write a custom serializer for the Contract itself as I would then have to modify it each time the properties change.

    The ideal solution is being able to tag certain decimal properties with the converter attribute so it can handle it.

    The other way I was thinking of going was to make a custom class for the decimal properties with an implicit conversion from decimal, however that gets more complicated as some properties are calculated properties based on previous results.

    WORKAROUND

    I have a work-around for my use case, but it uses reflection to obtain a private variable in the serializer.

    var binding = BindingFlags.NonPublic | BindingFlags.Instance;
    var writer = serializer.GetType()
                           .GetMethod("GetInternalSerializer", binding)
                           ?.Invoke(serializer, null);
    var parent = writer?.GetType()
                       .GetField("_serializeStack", binding)
                       ?.GetValue(writer) is List<object> stack 
                            && stack.Count > 1 ? stack[stack.Count - 2] as MyType: null;
    

    In my tested use cases this gives me the parent object, but it's not using the public API.

    解决方案

    What you want to do is to intercept and modify the value of a specific property of an object as it is being serialized while using default serialization for all other properties. This can be done with a custom ContractResolver that replaces the ValueProvider of the property in question when a specific attribute is applied.

    First, define the following attribute and contract resolver:

    [System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field, AllowMultiple = false)]
    public class JsonFormatAttribute : System.Attribute
    {
        public JsonFormatAttribute(string formattingString)
        {
            this.FormattingString = formattingString;
        }
    
        /// <summary>
        /// The format string to pass to string.Format()
        /// </summary>
        public string FormattingString { get; set; }
    
        /// <summary>
        /// The name of the underlying property that returns the object's culture, or NULL if not applicable.
        /// </summary>
        public string CulturePropertyName { get; set; }
    }
    
    public class FormattedPropertyContractResolver : DefaultContractResolver
    {
        protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
        {
            return base.CreateProperties(type, memberSerialization)
                .AddFormatting();
        }
    }
    
    public static class JsonContractExtensions
    {
        class FormattedValueProvider : IValueProvider
        {
            readonly IValueProvider baseProvider;
            readonly string formatString;
            readonly IValueProvider cultureValueProvider;
    
            public FormattedValueProvider(IValueProvider baseProvider, string formatString, IValueProvider cultureValueProvider)
            {
                this.baseProvider = baseProvider;
                this.formatString = formatString;
                this.cultureValueProvider = cultureValueProvider;
            }
    
            #region IValueProvider Members
    
            public object GetValue(object target)
            {
                var value = baseProvider.GetValue(target);
                var culture = cultureValueProvider == null ? null : (CultureInfo)cultureValueProvider.GetValue(target);
                return string.Format(culture ?? CultureInfo.InvariantCulture, formatString, value);
            }
    
            public void SetValue(object target, object value)
            {
                // This contract resolver should only be used for serialization, not deserialization, so throw an exception.
                throw new NotImplementedException();
            }
    
            #endregion
        }
    
        public static IList<JsonProperty> AddFormatting(this IList<JsonProperty> properties)
        {
            ILookup<string, JsonProperty> lookup = null;
    
            foreach (var jsonProperty in properties)
            {
                var attr = (JsonFormatAttribute)jsonProperty.AttributeProvider.GetAttributes(typeof(JsonFormatAttribute), false).SingleOrDefault();
                if (attr != null)
                {
                    IValueProvider cultureValueProvider = null;
                    if (attr.CulturePropertyName != null)
                    {
                        if (lookup == null)
                            lookup = properties.ToLookup(p => p.UnderlyingName);
                        var cultureProperty = lookup[attr.CulturePropertyName].FirstOrDefault();
                        if (cultureProperty != null)
                            cultureValueProvider = cultureProperty.ValueProvider;
                    }
                    jsonProperty.ValueProvider = new FormattedValueProvider(jsonProperty.ValueProvider, attr.FormattingString, cultureValueProvider);
                    jsonProperty.PropertyType = typeof(string);
                }
            }
            return properties;
        }
    }
    

    Next, define your object as follows:

    public class RootObject
    {
        [JsonFormat("{0:c}", CulturePropertyName = nameof(Culture))]
        public decimal Cost { get; set; }
    
        [JsonIgnore]
        public CultureInfo Culture { get; set; }
    
        public string SomeValue { get; set; }
    
        public string SomeOtherValue { get; set; }
    }
    

    Finally, serialize as follows:

    var settings = new JsonSerializerSettings
    {
        ContractResolver = new FormattedPropertyContractResolver
        {
            NamingStrategy = new CamelCaseNamingStrategy(),
        },
    };
    var json = JsonConvert.SerializeObject(root, Formatting.Indented, settings);
    

    Notes:

    1. Since you are not serializing the culture name, I can't see any way to deserialize the Cost property. Thus I threw an exception from the SetValue method.

      (And, even if you were serializing the culture name, since a JSON object is an unordered set of name/value pairs according the standard, there's no way to guarantee the culture name appears before the cost in the JSON being deserialized. This may be related to why Newtonsoft does not provide access to the parent stack. During deserialization there's no guarantee that required properties in the parent hierarchy have been read - or even that the parents have been constructed.)

    2. If you have to apply several different customization rules to your contracts, consider using ConfigurableContractResolver from How to add metadata to describe which properties are dates in JSON.Net.

    3. You may want to cache the contract resolver for best performance.

    4. Another approach would be to add a converter to the parent object that generates a default serialization to JObject by disabling itself temporarily, tweaks the returned JObject, then writes that out. For examples of this approach see JSON.Net throws StackOverflowException when using [JsonConvert()] or Can I serialize nested properties to my class in one operation with Json.net?.

    5. In comments you write, Inside WriteJson I cannot figure out how to access the parent object and it's properties. It should be possible to do this with a custom IValueProvider that returns a Tuple or similar class containing the parent and the value, which would be used in concert with a specific JsonConverter that expects such input. Not sure I'd recommend this though since it's extremely tricky.

    Working sample .Net fiddle.

    这篇关于NewtonSoft JsonConverter-访问其他属性的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆