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?

Rex M
  • 142,167
  • 33
  • 283
  • 313

7 Answers7

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://stackoverflow.com/a/13588192/1037948
/// </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 };
Community
  • 1
  • 1
drzaus
  • 24,171
  • 16
  • 142
  • 201
  • 1
    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)) – user576838 Feb 12 '13 at 19:19
  • @user576838 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! – drzaus Feb 12 '13 at 22:05
  • @user576838 - added extra check for EF – drzaus Jun 05 '13 at 20:52
  • 1
    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();` **To this** `if (!Ignores.ContainsKey(type)) Ignores[type] = new HashSet(StringComparer.OrdinalIgnoreCase);` – abraganza Oct 05 '15 at 18:23
  • 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); }` – abraganza Oct 05 '15 at 19:07
  • @abraganza 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](https://msdn.microsoft.com/en-us/library/bb353005(v=vs.110).aspx), right? – drzaus Oct 06 '15 at 20:46
  • 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. :) – abraganza Nov 05 '15 at 14:15
  • @abraganza - 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. – drzaus Nov 06 '15 at 14:46
  • I noticed that property.DeclaringType.BaseType doesn't exist in .NET Core 1.1 – Mark Entingh Apr 02 '17 at 22:49
  • {"Error converting value {null} to type 'System.DateTime'. Path 'basePlace.creationTime', line 1, position 112."} – Hassan Faghihi Oct 31 '17 at 04:53
  • `var result = new IgnorableSerializerContractResolver(); //result.Ignore(typeof(BasePlace), "CreationTime"); result.Ignore(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 MapFiles { get; set; } public List ImageFiles { get; set; } }` – Hassan Faghihi Oct 31 '17 at 16:32
  • 1
    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))` – Tallmaris Nov 07 '17 at 17:23
  • 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. – J.D. Mallen Nov 04 '20 at 00:56
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; }
}
James Skemp
  • 8,018
  • 9
  • 64
  • 107
Ramón Esteban
  • 916
  • 7
  • 10
  • 4
    Can you give an explanation on your answer please? – Zulu Oct 10 '14 at 23:00
  • 2
    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. – baHI Nov 25 '15 at 12:16
  • 6
    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. – James Skemp Jan 11 '16 at 16:15
  • 5
    This would cause the properties to be ignored in all serialization, not only in the specific one as required. – Danny Varod Feb 11 '16 at 18:25
  • this is not works for newtonsoft json serialization. – Sasi Dhivya Oct 11 '17 at 12:05
  • 1
    @SasiDhivya, it works for me. It is in the Newtonsoft.Json namespace, actually. Json.NET Version 11.0.2 – Jen-Ari Sep 14 '18 at 14:12
  • According to Newtonsoft: you can use either JsonIgnore or NonSerialized: https://www.newtonsoft.com/json/help/html/SerializationAttributes.htm#JsonIgnoreAttribute . Note that NonSerialized can only be set on Fields, not on Properties: https://learn.microsoft.com/en-us/dotnet/api/system.nonserializedattribute?redirectedfrom=MSDN&view=netframework-4.8 For me - both worked – ShayD Jan 28 '20 at 16:52
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;
   }
}
Marcus Mangelsdorf
  • 2,852
  • 1
  • 30
  • 40
Randolpho
  • 55,384
  • 17
  • 145
  • 179
  • 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? – xr280xr Oct 23 '19 at 17:09
  • @xr280xr I'm not sure. It's been a few years since this was posted... maybe it *used* to have an `Instance` property? – Randolpho Oct 23 '19 at 17:17
  • Maybe, but they still have it in [their example](https://www.newtonsoft.com/json/help/html/ConditionalProperties.htm#IContractResolver). I was trying to figure out why which is how I ended up here. – xr280xr Oct 23 '19 at 17:49
  • 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. – Randolpho Oct 23 '19 at 17:55
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);
Steve Rukuts
  • 9,167
  • 3
  • 50
  • 72
  • 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... ;) – drzaus Jun 12 '13 at 19:27
  • 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. – Steve Rukuts Jun 13 '13 at 11:46
  • So I was reusing this `Expression` trick, when I ran into a stumbling block with nested properties and EntityFramework `DbSet.Include` -- [see full explanation](http://stackoverflow.com/a/17220748/1037948), but basically parsing the `Expression.ToString` gives the "fully-qualified" property name in comparable time. – drzaus Jun 20 '13 at 18:41
  • 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(m => m.Address.ZipCode) (assuming you code it to work this way) then ZipCode won't be serialized for both Address and ShippingAddress! – baHI Nov 25 '15 at 12:20
  • 2
    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. – baHI Nov 25 '15 at 12:41
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.

frattaro
  • 3,241
  • 1
  • 16
  • 10
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;
}
Baron Ch'ng
  • 181
  • 8
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.)

cmeeren
  • 3,890
  • 2
  • 20
  • 50