2017-12-18 235 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'來代替我的具體用例。我已經接受了這個答案,因爲它回答了我所問的問題,現在我必須找出一種處理文化問題的方法,可以在更高一級的課程中進行,但我知道要走下去的正確道路。 –

相關問題