2017-12-18 184 views
2

我有一个需要格式化输出json的十进制货币,与文化指定我的对象我序列化,对象可以嵌套,所以我不能预设序列化器中的选项。目前我这样做的方式是使用格式化输出的额外字符串属性。NewtonSoft JsonConverter - 访问其他属性

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

我的问题是如何访问转换器包含对象的文化财产。

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请求示例JSON。

对于一个Contract类别的数组,每个类都有一个Cost和一个Culture。

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

实际的对象要复杂得多,嵌套Assets的多个字段会有自己的数字。另外并非所有小数都是货币。

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

理想的解决方案是能够使用转换器属性标记某些小数属性,以便它可以处理它。

我想去的另一种方式是用十进制隐式转换为小数属性创建一个自定义类,但是由于某些属性是基于以前的结果计算的属性,因此会变得更加复杂。

替代方法

我有一个变通为我的使用情况,但它使用反射来获得串行私有变量。

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。

+0

您是否试过编写'MyCurrencyConverter'? (PS你的第一个代码块不编译) – DavidG

+0

@DavidG是的,但问题是在writeJson里面我无法弄清楚如何访问父对象和它的属性。 –

+0

给我们你迄今为止的,然后我们可以提供帮助。 – DavidG

回答

2

你想要做的是拦截和修改对象的特定属性的值,因为它正在被序列化,而对所有其他属性使用默认序列化。这可以通过custom ContractResolver来完成,该特征在应用特定属性时替换所述属性的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对象是无序组根据standard名称/值对的,有没有办法保证文化的名字出现在成本之前这可能与为什么Newtonsoft无法访问父堆栈有关,在反序列化过程中,不能保证父层次结构中所需的属性已被读取 - 或者甚至已经构建父项。)

  2. 如果您必须将多个不同的自定义规则应用于您的合同,请考虑使用来自How to add metadata to describe which properties are dates in JSON.Net的。

  3. 您可能想要cache the contract resolver以获得最佳性能。

  4. 另一种方法是向父对象添加一个转换器,该转换器通过临时禁用自己生成默认序列号JObject,调整返回的JObject,然后写出。有关此方法的示例,请参阅JSON.Net throws StackOverflowException when using [JsonConvert()]Can I serialize nested properties to my class in one operation with Json.net?

  5. 在你写的评论中,在WriteJson里我无法弄清楚如何访问父对象及其属性。应该可以用自定义IValueProvider来做到这一点,该自定义返回一个Tuple或包含父项和值的类似类,它将与预期此类输入的特定JsonConverter一致使用。不知道我会推荐这个,因为它非常棘手。

工作样本.Net fiddle

+0

辉煌,谢谢,这解决了我的问题,我通过使用'DataTypeAttribute'来代替我的具体用例。我已经接受了这个答案,因为它回答了我所问的问题,现在我必须找出一种处理文化问题的方法,可以在更高一级的课程中进行,但我知道要走下去的正确道路。 –

相关问题