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

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

问题描述

我需要将十进制的输出 json 格式化为货币,并指定我正在序列化的对象的文化,该对象可以嵌套,因此我无法在序列化程序中预设选项.我目前的做法是使用额外的字符串属性来格式化输出.

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

我有很多属性要处理,我不关心反序列化,JsonObject 被不同的语言用来填充 PDF,所以我想要字符串值.

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.

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

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

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

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

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

作为请求的示例 JSON.

As Requested sample JSON.

对于一个 Contract 类数组,每个类都有一个 Cost 和一个 Culture.

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.

解决方法

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

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;

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

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

推荐答案

您要做的是在对象的特定属性被序列化时拦截和修改它的值,同时对所有其他属性使用默认序列化.这可以通过 自定义 ContractResolver 替换 ValueProvider应用特定属性时有问题的属性.

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

最后,序列化如下:

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

注意事项:

  1. 由于您没有序列化区域性名称,我看不到任何反序列化 Cost 属性的方法.因此,我从 SetValue 方法.

  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.

(而且,即使您正在序列化文化名称,因为 JSON 对象是 无序 一组名称/值对,根据 标准,无法保证在反序列化的 JSON 中的成本之前出现文化名称.这可能与 Newtonsoft 为什么不提供对父堆栈的访问.在反序列化期间,无法保证已读取父层次结构中的所需属性 - 甚至无法保证已构建父级.)

(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.)

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

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.

您可能希望缓存合约解析器以获得最佳性能.

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

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

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?.

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

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.

工作示例 .Net fiddle.

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

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