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。

对于一组合同类,每个类都有成本和文化。

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

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

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

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

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

替代方法

我有一个解决我的用例的方法,但它使用reflection来获取序列化程序中的私有变量。

 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 stack && stack.Count > 1 ? stack[stack.Count - 2] as MyType: null; 

在我测试的用例中,这给了我父对象,但它没有使用公共API。

您要做的是在对象进行序列化时拦截和修改对象的特定属性的值,同时对所有其他属性使用默认序列化。 这可以通过自定义ContractResolver来完成,该自定义ContractResolver在应用特定属性时替换相关属性的ValueProvider

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

 [System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field, AllowMultiple = false)] public class JsonFormatAttribute : System.Attribute { public JsonFormatAttribute(string formattingString) { this.FormattingString = formattingString; } ///  /// The format string to pass to string.Format() ///  public string FormattingString { get; set; } ///  /// The name of the underlying property that returns the object's culture, or NULL if not applicable. ///  public string CulturePropertyName { get; set; } } public class FormattedPropertyContractResolver : DefaultContractResolver { protected override IList 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 AddFormatting(this IList properties) { ILookup 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方法中抛出exception。

    (而且,即使您正在序列化文化名称,由于JSON对象是根据标准 无序的名称/值对集合 ,因此无法保证在反序列化JSON中的成本之前出现文化名称。这可能是与为什么Newtonsoft不提供对父堆栈的访问有关。在反序列化期间,不能保证已经读取了父层次结构中的必需属性 – 或者甚至已经构造了父级。)

  2. 如果必须对合同应用多个不同的自定义规则,请考虑使用如何添加元数据中的 ConfigurableContractResolver 来描述JSON.Net中哪些属性是日期 。

  3. 您可能希望缓存合同解析程序以获得最佳性能。

  4. 另一种方法是向父对象添加一个转换器,通过暂时禁用自身来生成对JObject的默认序列化,调整返回的JObject ,然后将其写出来。 有关此方法的示例,请参阅JSON.Net在使用[JsonConvert()]时抛出StackOverflowException或者我可以使用Json.net在一次操作中将嵌套属性序列化到我的类吗? 。

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

工作样本.Net小提琴 。