我需要将十进制的输出 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
类数组。
[{ FormattedCost : "£5000.00"}, { FormattedCost : "$8000.00"}, { FormattedCost : "€599.00"}]
实际对象要复杂得多,多个字段具有嵌套资产,这些资产将具有自己的图形。此外,并非所有小数点都是货币。
我真的不想为合约本身编写自定义序列化程序,因为每次属性更改时我都必须修改它。
理想的解决方案是能够使用 converter 属性标记某些十进制属性,以便它可以处理它。
我想到的另一种方法是为十进制属性创建一个自定义类,从十进制隐式转换,但是这变得更加复杂,因为某些属性是基于以前结果的计算属性。
解决方法
我的用例有一个解决方法,但它使用反射来获取序列化程序中的私有变量。
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
来完成,该在应用特定属性时替换相关属性的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);
笔记:
由于您没有序列化区域性名称,因此我看不到任何反序列化
Cost
属性的方法。 因此,我从SetValue
方法中抛出了一个异常。(而且,即使序列化区域性名称,由于 JSON 对象是一组无序的名称/值对,因此无法保证区域性名称出现在反序列化的 JSON 中的成本之前。 这可能与 Newtonsoft 不提供对父堆栈的访问的原因有关。 在反序列化期间,无法保证已读取父层次结构中的必需属性 - 甚至无法保证已构造父层次结构。
如果必须对合同应用多个不同的自定义规则,请考虑使用如何添加元数据中的
ConfigurableContractResolver
来描述哪些属性是 JSON.Net 中的日期。您可能希望缓存协定解析程序以获得最佳性能。
另一种方法是向父对象添加一个转换器,该转换器通过暂时禁用自身来生成默认序列化以
JObject
,调整返回的JObject
,然后将其写出。 有关此方法的示例,请参阅使用[JsonConvert()]
时 JSON.Net 抛出 StackOverflowException 或是否可以使用 Json.net 在一个操作中将嵌套属性序列化到我的类?。在您编写的注释中,Inside WriteJson 我无法弄清楚如何访问父对象及其属性。应该可以使用自定义
IValueProvider
来执行此操作,该返回包含父级和值的Tuple
或类似类,该类将与需要此类输入的特定JsonConverter
一起使用。 不确定我会推荐这个,因为它非常棘手。
工作示例 .Net 小提琴。