Exclude property from serialization via custom attribute (json.net)
Asked Answered
H

7

65

I need to be able to control how/whether certain properties on a class are serialized. The simplest case is [ScriptIgnore]. However, I only want these attributes to be honored for this one specific serialization situation I am working on - if other modules downstream in the application also want to serialize these objects, none of these attributes should get in the way.

So my thought is to use a custom attribute MyAttribute on the properties, and initialize the specific instance of JsonSerializer with a hook that knows to look for that attribute.

At first glance, I don't see any of the available hook points in JSON.NET will provide the PropertyInfo for the current property to do such an inspection - only the property's value. Am I missing something? Or a better way to approach this?

Hotbed answered 27/11, 2012 at 15:55 Comment(0)
H
51

You have a few options. I recommend you read the Json.Net documentation article on the subject before reading below.

The article presents two methods:

  1. Create a method that returns a bool value based on a naming convention that Json.Net will follow to determine whether or not to serialize the property.
  2. Create a custom contract resolver that ignores the property.

Of the two, I favor the latter. Skip attributes altogether -- only use them to ignore properties across all forms of serialization. Instead, create a custom contract resolver that ignores the property in question, and only use the contract resolver when you want to ignore the property, leaving other users of the class free to serialize the property or not at their own whim.

Edit To avoid link rot, I'm posting the code in question from the article

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;
   }
}
Hurryscurry answered 27/11, 2012 at 16:4 Comment(4)
Why do they use the new modifier when declaring Instance? DefaultContractResolver does not declare an Instance member. Additionally, what is the purpose of Instance even being declared here?Saire
@Saire I'm not sure. It's been a few years since this was posted... maybe it used to have an Instance property?Hurryscurry
Maybe, but they still have it in their example. I was trying to figure out why which is how I ended up here.Saire
It seems unnecessary to me. I can’t verify right now but if you’re about to use the example, try removing new and see what happens.Hurryscurry
C
75

Here's a generic reusable "ignore property" resolver based on the accepted answer:

/// <summary>
/// Special JsonConvert resolver that allows you to ignore properties.  See https://mcmap.net/q/25175/-exclude-property-from-serialization-via-custom-attribute-json-net
/// </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;
    }
}

And usage:

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 };
Connel answered 24/1, 2013 at 20:28 Comment(13)
I know this has been answered, but I found when seralizing EF models, you need to compare the basetype. EX) if (this.IsIgnored(property.DeclaringType.BaseType, property.PropertyName))Hellespont
@Hellespont that is a good point. I think it depends on your flavor (code-first vs database-first) -- I think EF makes "random" proxy classes in the case of database-first, but might not in code-first? Probably safer to include the check though!Connel
@Hellespont - added extra check for EFConnel
In the Ignore method when we start the bucket, it may make sense to declare the HashSet as follows to ignore case for the propertyname Change this if (!Ignores.ContainsKey(type)) Ignores[type] = new HashSet<string>(); To this if (!Ignores.ContainsKey(type)) Ignores[type] = new HashSet<string>(StringComparer.OrdinalIgnoreCase);Severance
Also the foreach loop in the Ignore method should have a continue if the property is already in the hashset. ` foreach (var prop in propertyName) { if (Ignores[type].Contains(prop)) continue; Ignores[type].Add(prop); }`Severance
@Severance I can see how ignoring case can make it easier to use, but why do you need to check if the property name is already in the hashset? .Add just returns false if it's already present, right?Connel
You are correct @drzaus, but the reason for the continue is to short circuit the processing. We have already checked the collection, so it doesn't really make sense to access it again. I was just saying that we not do the additional hit. :)Severance
@Severance - you're not really short-circuiting much processing; the only difference between Contains and Add is that Contains doesn't initialize the internal buckets before doing the same collision check. They both stop if there's a collision at pretty much the same point. If anything, you're actually accessing the collection twice with Contains + Add; but I am considering the scenario where you only set up the Ignore in one place, so I would think you're unlikely to accidentally add the same property twice. Either way it's a HashSet, so we're not optimizing much.Connel
I noticed that property.DeclaringType.BaseType doesn't exist in .NET Core 1.1Lamplighter
{"Error converting value {null} to type 'System.DateTime'. Path 'basePlace.creationTime', line 1, position 112."}Fatten
var result = new IgnorableSerializerContractResolver(); //result.Ignore(typeof(BasePlace), "CreationTime"); result.Ignore<BasePlace>(w => w.CreationTime); result.Ignore(typeof(BasePlace), "creationTime");/*??? small??*/ result.Ignore(typeof(BasePlace), "basePlace.creationTime"); return result; None Works... my model: public class NestInputModel { public BasePlace BasePlace { get; set; } public List<string> MapFiles { get; set; } public List<string> ImageFiles { get; set; } }Fatten
Interfaces have a null BaseType which crashes the Contains. We should check for that as well || (property.DeclaringType.BaseType != null && this.IsIgnored(property.DeclaringType.BaseType, property.PropertyName))Daina
If you've set NamingStrategy = new CamelCaseNamingStrategy(); in the constructor, and your Ignores aren't working, follow @abraganza's tip about the StringComparer. However, if you want to be more strict about it, you can also convert the prop names to camelCase on the fly as you add them to the HashSet if that NamingStrategy is set. Probably overkill in most cases, but it's an option.Curren
C
69

Use the JsonIgnore attribute.

For example, to exclude Id:

public class Person {
    [JsonIgnore]
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}
Camiecamila answered 10/10, 2014 at 22:46 Comment(7)
Can you give an explanation on your answer please?Painterly
JsonIgnore may work on classes under control, but not 3rd party classes. And even when having out custom classes, sometimes we might need only part of the class to be serialized.Earthy
I've added an example. I don't think this answer answers the question, but it did assist me with what I was trying to do.Necessitarianism
This would cause the properties to be ignored in all serialization, not only in the specific one as required.Consensual
this is not works for newtonsoft json serialization.Electrophorus
@SasiDhivya, it works for me. It is in the Newtonsoft.Json namespace, actually. Json.NET Version 11.0.2Jaquelin
According to Newtonsoft: you can use either JsonIgnore or NonSerialized: newtonsoft.com/json/help/html/… . Note that NonSerialized can only be set on Fields, not on Properties: learn.microsoft.com/en-us/dotnet/api/… For me - both workedBova
H
51

You have a few options. I recommend you read the Json.Net documentation article on the subject before reading below.

The article presents two methods:

  1. Create a method that returns a bool value based on a naming convention that Json.Net will follow to determine whether or not to serialize the property.
  2. Create a custom contract resolver that ignores the property.

Of the two, I favor the latter. Skip attributes altogether -- only use them to ignore properties across all forms of serialization. Instead, create a custom contract resolver that ignores the property in question, and only use the contract resolver when you want to ignore the property, leaving other users of the class free to serialize the property or not at their own whim.

Edit To avoid link rot, I'm posting the code in question from the article

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;
   }
}
Hurryscurry answered 27/11, 2012 at 16:4 Comment(4)
Why do they use the new modifier when declaring Instance? DefaultContractResolver does not declare an Instance member. Additionally, what is the purpose of Instance even being declared here?Saire
@Saire I'm not sure. It's been a few years since this was posted... maybe it used to have an Instance property?Hurryscurry
Maybe, but they still have it in their example. I was trying to figure out why which is how I ended up here.Saire
It seems unnecessary to me. I can’t verify right now but if you’re about to use the example, try removing new and see what happens.Hurryscurry
C
30

Here is a method based on drzaus' excellent serializer contract which uses lambda expressions. Simply add it to the same class. After all, who doesn't prefer the compiler to do the checking for them?

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

You can now ignore properties easily and fluently:

contract.Ignore<Node>(node => node.NextNode)
    .Ignore<Node>(node => node.AvailableNodes);
Corned answered 20/5, 2013 at 10:18 Comment(5)
actually, I love the MemberExpression trick -- it works like Reflection, but feels less clunky. I'm going to use this all over the place. Hope it's still performant... ;)Connel
It's certainly less speedy than your version but I feel the tradeoff for the compiler checking it for you is worth it. Unless you've put this in the middle of an O(N^2) loop or something I doubt it would affect anything. Preliminary reading tells me it's significantly faster than reflection anyway.Corned
So I was reusing this Expression trick, when I ran into a stumbling block with nested properties and EntityFramework DbSet.Include -- see full explanation, but basically parsing the Expression.ToString gives the "fully-qualified" property name in comparable time.Connel
Nice solution. The major thing we have to realize that this is a dynamic way of excluding properties. But there's a catch: if you use Model.Address and Model.ShippingAddress and you say contract.Ignore<Model>(m => m.Address.ZipCode) (assuming you code it to work this way) then ZipCode won't be serialized for both Address and ShippingAddress!Earthy
Also a bit patch. Use: this.Ignore(body.Member.DeclaringType, propertyName) instaed of typeof(TModel). If you do so, expression m => m.Address.ZipCode would be also interpreted correctly.Earthy
D
4

I don't care to set the property names as strings, in case they ever change it would break my other code.

I had several "view modes" on the objects I needed to serialized, so I ended up doing something like this in the contract resolver (view mode provided by constructor argument):

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

Where my objects look like this:

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

The downside to this would be the bit of reflection in the resolver, but I think it's worth it to have more maintainable code.

Donaldson answered 7/3, 2014 at 14:27 Comment(0)
S
1

I had good results with the combination of both drzaus and Steve Rukuts answers. However, I face a problem when I set JsonPropertyAttribute with a different name or caps for the property. For example:

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

Include UnderlyingName into consideration solves the problem:

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;
}
Shotgun answered 29/11, 2017 at 18:1 Comment(0)
M
0

If you are willing to use F# (or simply use an API not optimized for C#), the FSharp.JsonSkippable library allows you to control in a simple and strongly typed manner whether to include a given property when serializing (and determine whether a property was included when deserializing), and moreover, to control/determine exclusion separately of nullability. (Full disclosure: I'm the author of the library.)

Mcgill answered 4/11, 2018 at 17:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.