通过自定义属性(json.net),将属性排除在序列化之外

65

我需要能够控制类上的某些属性如何/是否进行序列化。最简单的情况是[ScriptIgnore]。然而,我只想要这些属性在我正在处理的这个特定序列化情况下得到保留 - 如果应用程序中下游的其他模块也想要序列化这些对象,则不应该有任何这些属性的影响。

我的想法是在属性上使用自定义属性 MyAttribute ,并初始化 JsonSerializer 的特定实例以具有知道查找该属性的钩子。

乍一看,我似乎没有看到JSON.NET中可用的任何挂接点将提供 PropertyInfo 来执行这样的检查 - 仅提供属性值。我错过了什么吗?或者有更好的方法来解决这个问题吗?

7个回答

75

这是一个通用的可重复使用的“忽略属性”解析器,基于被接受的答案

/// <summary>
/// Special JsonConvert resolver that allows you to ignore properties.  See https://dev59.com/AGYr5IYBdhLWcg3wi63d#13588192
/// </summary>
public class IgnorableSerializerContractResolver : DefaultContractResolver {
    protected readonly Dictionary<Type, HashSet<string>> Ignores;

    public IgnorableSerializerContractResolver() {
        this.Ignores = new Dictionary<Type, HashSet<string>>();
    }

    /// <summary>
    /// Explicitly ignore the given property(s) for the given type
    /// </summary>
    /// <param name="type"></param>
    /// <param name="propertyName">one or more properties to ignore.  Leave empty to ignore the type entirely.</param>
    public void Ignore(Type type, params string[] propertyName) {
        // start bucket if DNE
        if (!this.Ignores.ContainsKey(type)) this.Ignores[type] = new HashSet<string>();

        foreach (var prop in propertyName) {
            this.Ignores[type].Add(prop);
        }
    }

    /// <summary>
    /// Is the given property for the given type ignored?
    /// </summary>
    /// <param name="type"></param>
    /// <param name="propertyName"></param>
    /// <returns></returns>
    public bool IsIgnored(Type type, string propertyName) {
        if (!this.Ignores.ContainsKey(type)) return false;

        // if no properties provided, ignore the type entirely
        if (this.Ignores[type].Count == 0) return true;

        return this.Ignores[type].Contains(propertyName);
    }

    /// <summary>
    /// The decision logic goes here
    /// </summary>
    /// <param name="member"></param>
    /// <param name="memberSerialization"></param>
    /// <returns></returns>
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) {
        JsonProperty property = base.CreateProperty(member, memberSerialization);

        if (this.IsIgnored(property.DeclaringType, property.PropertyName)
        // need to check basetype as well for EF -- @per comment by user576838
        || this.IsIgnored(property.DeclaringType.BaseType, property.PropertyName)) {
            property.ShouldSerialize = instance => { return false; };
        }

        return property;
    }
}

用法:

var jsonResolver = new IgnorableSerializerContractResolver();
// ignore single property
jsonResolver.Ignore(typeof(Company), "WebSites");
// ignore single datatype
jsonResolver.Ignore(typeof(System.Data.Objects.DataClasses.EntityObject));
var jsonSettings = new JsonSerializerSettings() { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, ContractResolver = jsonResolver };

1
我知道这个问题已经有答案了,但是我发现在序列化EF模型时,你需要比较基础类型。例如: if (this.IsIgnored(property.DeclaringType.BaseType, property.PropertyName)) - user576838
1
在 Ignore 方法中,当我们启动 bucket 时,将 HashSet 声明为以下形式以忽略 propertyname 的大小写可能是有意义的。更改为 if (!Ignores.ContainsKey(type)) Ignores[type] = new HashSet<string>(StringComparer.OrdinalIgnoreCase); - abraganza
同时,在 Ignore 方法中的 foreach 循环应该在属性已经存在于哈希集合中时继续。 ` foreach (var prop in propertyName) { if (Ignores[type].Contains(prop)) continue; Ignores[type].Add(prop); }` - abraganza
@abraganza 我能理解忽略大小写可以使使用更容易,但为什么需要检查属性名是否已经在哈希集中?.Add如果已经存在就会返回false,对吧? - drzaus
1
接口具有“null”基类型,这会导致“Contains”崩溃。我们也应该检查这一点 || (property.DeclaringType.BaseType != null && this.IsIgnored(property.DeclaringType.BaseType, property.PropertyName)) - Tallmaris
显示剩余8条评论

69

使用JsonIgnore属性。

例如,要排除Id

public class Person {
    [JsonIgnore]
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

4
你能否解释一下你的答案? - Zulu
2
JsonIgnore 可以在受控类上工作,但不适用于第三方类。即使有自定义类,有时我们可能只需要序列化类的一部分。 - baHI
6
我已添加一个示例。我认为这个答案并没有回答问题,但它确实帮助了我完成我所要做的事情。 - James Skemp
5
这将导致所有序列化中都忽略这些属性,而不仅仅是在所需的特定序列化中。 - Danny Varod
1
@SasiDhivya,这对我有用。它实际上在Newtonsoft.Json命名空间中。 Json.NET版本11.0.2 - Jen-Ari
显示剩余2条评论

51

你有几个选项。我建议您先阅读Json.Net文档关于此主题的文章,然后再阅读下面的内容。

该文章提出了两种方法:

  1. 创建一个方法,该方法基于一种命名约定返回一个bool值,Json.Net将遵循该约定来确定是否序列化该属性。
  2. 创建一个自定义合同解析器,忽略该属性。

在这两种方法中,我更喜欢后者。完全跳过属性 -- 仅使用属性来忽略所有形式的序列化中的属性。相反,创建一个自定义合同解析器,忽略相关属性,并且只有在您想要忽略该属性时才使用合同解析器,使类的其他用户可以自由地决定是否序列化该属性。

编辑为了避免链接失效,我将发布来自文章的相关代码

public class ShouldSerializeContractResolver : DefaultContractResolver
{
   public new static readonly ShouldSerializeContractResolver Instance =
                                 new ShouldSerializeContractResolver();

   protected override JsonProperty CreateProperty( MemberInfo member,
                                    MemberSerialization memberSerialization )
   {
      JsonProperty property = base.CreateProperty( member, memberSerialization );

      if( property.DeclaringType == typeof(Employee) &&
            property.PropertyName == "Manager" )
      {
         property.ShouldSerialize = instance =>
         {
            // replace this logic with your own, probably just  
            // return false;
            Employee e = (Employee)instance;
            return e.Manager != e;
         };
      }

      return property;
   }
}

为什么在声明“Instance”时要使用“new”修饰符?“DefaultContractResolver”没有声明“Instance”成员。此外,这里声明“Instance”的目的是什么? - xr280xr
@xr280xr 我不确定。这个帖子已经发布几年了...也许它曾经有一个“Instance”属性? - Randolpho
也许吧,但是在他们的示例中仍然有这个功能(https://www.newtonsoft.com/json/help/html/ConditionalProperties.htm#IContractResolver)。我一直在试图弄清楚原因,这就是我来到这里的原因。 - xr280xr
对我来说似乎是不必要的。我现在无法验证,但如果你要使用这个例子,请尝试删除“new”并查看会发生什么。 - Randolpho

30

以下是基于 drzaus 的优秀序列化器合同的方法,它使用 lambda 表达式。只需将其添加到同一类中即可。毕竟,有谁不喜欢编译器为他们进行检查呢?

public IgnorableSerializerContractResolver Ignore<TModel>(Expression<Func<TModel, object>> selector)
{
    MemberExpression body = selector.Body as MemberExpression;

    if (body == null)
    {
        UnaryExpression ubody = (UnaryExpression)selector.Body;
        body = ubody.Operand as MemberExpression;

        if (body == null)
        {
            throw new ArgumentException("Could not get property name", "selector");
        }
    }

    string propertyName = body.Member.Name;
    this.Ignore(typeof (TModel), propertyName);
    return this;
}

现在您可以轻松、流畅地忽略属性:

contract.Ignore<Node>(node => node.NextNode)
    .Ignore<Node>(node => node.AvailableNodes);

实际上,我喜欢 MemberExpression 技巧 - 它的工作原理类似于反射,但感觉不那么笨重。我将在许多地方使用它。希望它仍然具有良好的性能... ;) - drzaus
它肯定比你的版本慢,但我觉得为了让编译器为你检查而做出的权衡是值得的。除非你把它放在O(N^2)循环中间或者其他类似情况,否则我怀疑它会对任何东西产生影响。初步阅读告诉我,它比反射要快得多。 - Steve Rukuts
所以我一直在重复使用这个“Expression”技巧,但当我遇到嵌套属性和EntityFramework的“DbSet.Include”时,我遇到了一个障碍 - [请参见完整解释](https://dev59.com/-HRB5IYBdhLWcg3wSVYI#17220748),但基本上解析“Expression.ToString”可以在可比较的时间内给出“完全限定”的属性名称。 - drzaus
不错的解决方案。我们必须意识到这是一种动态排除属性的方法。但有一个问题:如果您使用Model.Address和Model.ShippingAddress,并且您说contract.Ignore<Model>(m => m.Address.ZipCode)(假设您编写它以使其工作),那么ZipCode将不会为Address和ShippingAddress序列化! - baHI
2
还有一个小补丁。使用:this.Ignore(body.Member.DeclaringType, propertyName) 代替 typeof(TModel)。如果这样做,表达式 m => m.Address.ZipCode 也会被正确解释。 - baHI

4

如果属性名称发生更改,我不想将属性名称设置为字符串,否则会破坏我的其他代码。

我有几个需要序列化的对象的“视图模式”,因此我最终在合同解析器中执行了以下操作(通过构造函数参数提供视图模式):

protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
    JsonProperty property = base.CreateProperty(member, memberSerialization);
    if (viewMode == ViewModeEnum.UnregisteredCustomer && member.GetCustomAttributes(typeof(UnregisteredCustomerAttribute), true).Length == 0)
    {
        property.ShouldSerialize = instance => { return false; };
    }

    return property;
}

我的对象看起来像这样:

public interface IStatement
{
    [UnregisteredCustomer]
    string PolicyNumber { get; set; }

    string PlanCode { get; set; }

    PlanStatus PlanStatus { get; set; }

    [UnregisteredCustomer]
    decimal TotalAmount { get; }

    [UnregisteredCustomer]
    ICollection<IBalance> Balances { get; }

    void SetBalances(IBalance[] balances);
}

这样做的缺点是解析器中会有一些反射,但我认为这值得拥有更易维护的代码。

1

我使用了drzaus和Steve Rukuts的答案组合后取得了良好的结果。但是,当我为属性设置不同名称或大写字母时,我遇到了问题。例如:

[JsonProperty("username")]
public string Username { get; set; }

将UnderlyingName纳入考虑可以解决这个问题:

protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
    JsonProperty property = base.CreateProperty(member, memberSerialization);

    if (this.IsIgnored(property.DeclaringType, property.PropertyName)
        || this.IsIgnored(property.DeclaringType, property.UnderlyingName)
        || this.IsIgnored(property.DeclaringType.BaseType, property.PropertyName)
        || this.IsIgnored(property.DeclaringType.BaseType, property.UnderlyingName))
    {
        property.ShouldSerialize = instance => { return false; };
    }

    return property;
}

0

如果您愿意使用F#(或者只是使用未针对C#进行优化的API),FSharp.JsonSkippable库允许您以一种简单而强类型的方式控制是否在序列化时包含给定属性(并确定反序列化时是否包含属性),并且,分别控制/确定可空性的排除。 (完全透明:我是该库的作者。)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接