5

I'm encountering an odd situation where IgnoreDataMember isn't doing the job, but JsonIgnore does.

In this case I'm inheriting from a class with a public {get; set;} property, and I'm choosing to override the setter with a NotSupportedException. I only want the serializer to be able to set the property, not the user, but I want the user to get it, so I've done the following:

[DataContract]
public class BaseObject : BaseInterface
{
     [DataMember]
     public virtual double information { get; set; }
}

[DataContract]
public class ServerGeneratedObject : BaseObject
{
     [IgnoreDataMember]
     public override double information {
         get { return server_set_information; }
         set { throw new NotSupportedException("Server generated property"); }
     }

     [DataMember(Name="information")]
     private double server_set_information { get; set; }
}

Unfortunately, this raises an error "A member with the name 'information' already exists on 'ServerGeneratedObject'. Use the JsonPropertyAttribute to specify another name."

If, however, I use the [JsonIgnore] attribute, this works as expected. This appears to be due to this section of the data contract resolver (code currently lives here):

bool flag2 = JsonTypeReflector.GetAttribute<JsonIgnoreAttribute>(attributeProvider) != null ||
    JsonTypeReflector.GetAttribute<JsonExtensionDataAttribute>(attributeProvider) != null ||
    JsonTypeReflector.GetAttribute<NonSerializedAttribute>(attributeProvider) != null;
if (memberSerialization != MemberSerialization.OptIn)
{
    bool flag3 = JsonTypeReflector.GetAttribute<IgnoreDataMemberAttribute>(attributeProvider) != null;
    property.Ignored = flag2 | flag3;
}

The property isn't correctly being set to 'ignored' because it's in 'OptIn' mode, but if that's the case, I have no idea why the inherited "information" property is being "opted in" because the "DataMember" attribute is not supposed to be inheritable. I filed a bug here in case this isn't expected behaviour.

Is there something I can do here? I'm trying to avoid using any "Newtonsoft" attributes on my public data model, because I don't want people using my client library object model to necessarily have to reference the Newtonsoft assembly.

Alain
  • 26,663
  • 20
  • 114
  • 184
  • I just tried to serialize your class with `DataContractJsonSerializer`, and it threw an exception **The data contract type 'Question38020614.ServerGeneratedObject' is not serializable with DataContractJsonSerializer because the data member 'information' is duplicated in its type hierarchy.** – dbc Jun 24 '16 at 19:31

2 Answers2

2

I suspect that the logic you are seeing is intended to make Json.NET consistent with DataContractJsonSerializer in situations where a property is marked with both [DataContract] and [IgnoreDataContract]. When this is done, [DataContract] will take precedence and the data contract serializer will output the property. E.g. serializing

[DataContract]
public class Test
{
    [DataMember]
    [IgnoreDataMember]
    public virtual double information { get; set; }
}

Results in {"information":0}.

(The logic may also be an efficiency tweak. In straightforward cases, if a type is marked with [DataContract], then [IgnoreDataMember] is superfluous, so there's no need to spend time checking for it with reflection.)

Perhaps because of this, both Json.NET and DataContractJsonSerializer will throw an exception serializing a derived class that overrides a data member property in its base class, marks the overridden property with [IgnoreDataMember], then adds an unrelated property with the same data member name. If you try to do this, Json.NET throws the exception you see -- and DataContractJsonSerializer also throws an exception:

System.Runtime.Serialization.SerializationException occurred
  Message="The data contract type 'Question38020614.ServerGeneratedObject' is not serializable with DataContractJsonSerializer because the data member 'information' is duplicated in its type hierarchy."
  Source="System.ServiceModel.Web"

Nevertheless, you can make Json.NET behave as desired by creating a custom contract resolver inheriting from DefaultContractResolver or CamelCasePropertyNamesContractResolver, overriding CreateProperty() and adding the desired logic:

public class IgnoreDataMemberContractResolver : DefaultContractResolver
{
    // As of 7.0.1, Json.NET suggests using a static instance for "stateless" contract resolvers, for performance reasons.
    // http://www.newtonsoft.com/json/help/html/ContractResolver.htm
    // http://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_Serialization_DefaultContractResolver__ctor_1.htm
    // "Use the parameterless constructor and cache instances of the contract resolver within your application for optimal performance."
    // See also https://stackoverflow.com/questions/33557737/does-json-net-cache-types-serialization-information
    static IgnoreDataMemberContractResolver instance;

    // Explicit static constructor to tell C# compiler not to mark type as beforefieldinit
    static IgnoreDataMemberContractResolver() { instance = new IgnoreDataMemberContractResolver(); }

    public static IgnoreDataMemberContractResolver Instance { get { return instance; } }

    protected override JsonProperty CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        if (memberSerialization == MemberSerialization.OptIn)
        {
            // Preserve behavior that [DataMember] supersedes [IgnoreDataMember] when applied in the same type
            // but not when appled to a base type.
            if (!property.Ignored
                && property.AttributeProvider.GetAttributes(typeof(IgnoreDataMemberAttribute), false).Any()
                && !property.AttributeProvider.GetAttributes(typeof(DataMemberAttribute), true).Any())
            {
                property.Ignored = true;
            }
        }
        return property;
    }
}

Then use it like:

var settings = new JsonSerializerSettings { ContractResolver = IgnoreDataMemberContractResolver.Instance };
var json = JsonConvert.SerializeObject(serverGeneratedObject, settings);
dbc
  • 104,963
  • 20
  • 228
  • 340
0

I was running into an issue where the above solution wasn't working because if you only "ignore" the property after it's been created, Newtonsoft will still blow up if your ignored property has the same name as another property you're trying to serialize. This avoids that by not "creating" the JsonProperty (which involves adding it to a property dictionary) at all:

/// <summary>Properties tagged with the system <see cref="IgnoreDataMemberAttribute"/>
/// should be ignored by the JSON serializer.
/// Due to a Newtonsoft JSON bug (https://github.com/JamesNK/Newtonsoft.Json/issues/943)
/// We need to use their own specific JsonIgnore attribute to effectively ignore a property.
/// This contract resolver aims to correct that.</summary>
public class RespectIgnoreDataMemberResolver : DefaultContractResolver
{
    /// <inheritdoc/>
    protected override List<MemberInfo> GetSerializableMembers(Type objectType)
    {
        return base.GetSerializableMembers(objectType)
            .Where(pi => !pi.IsAttributeDefinedFast<IgnoreDataMemberAttribute>())
            .ToList();
    }

    /// <inheritdoc/>
    protected override JsonProperty CreateProperty(MemberInfo member,
        MemberSerialization memberSerialization)
    {
        if (member.IsAttributeDefinedFast<IgnoreDataMemberAttribute>())
            return null;
        return base.CreateProperty(member, memberSerialization);
    }
}

IsAttributeDefinedFast is just a type extension method I have elsewhere that caches the result for future use:

    #region Cached Attribute Retrieval
    /// <summary>Cache of attributes retrieved for members.</summary>
    private static readonly ConcurrentDictionary<Tuple<string, Type, Type>, object>
        CachedPropertyAttributes = new ConcurrentDictionary<Tuple<string, Type, Type>, object>();

    /// <summary>Determines whether the member has the specified attribute defined.</summary>
    /// <typeparam name="T">The type of the attribute to look for.</typeparam>
    /// <param name="member">The member to check if an attribute is defined on.</param>
    /// <returns>True if the attribute is defined.</returns>
    public static bool IsAttributeDefinedFast<T>(this MemberInfo member)
    {
        return IsAttributeDefinedFast(member, typeof(T));
    }

    /// <summary>Determines whether the member has the specified attribute defined.</summary>
    /// <param name="member">The member to check if an attribute is defined on.</param>
    /// <param name="attributeType">The type of the attribute to look for.</param>
    /// <returns>True if the attribute is defined.</returns>
    public static bool IsAttributeDefinedFast(this MemberInfo member, Type attributeType)
    {
        return member.GetCustomAttributeFast(attributeType) != null;
    }

    /// <summary>Gets the specified attribute from the member.</summary>
    /// <typeparam name="T">The type of the attribute to look for.</typeparam>
    /// <param name="member">The member to get the custom attribute of.</param>
    /// <returns>True if the attribute is defined on the specified member.</returns>
    public static T GetCustomAttributeFast<T>(this MemberInfo member)
    {
        return (T)GetCustomAttributeFast(member, typeof(T));
    }

    /// <summary>Gets the specified attribute from the member.</summary>
    /// <param name="member">The member to get the custom attribute of.</param>
    /// <param name="attributeType">The type of the attribute to look for.</param>
    /// <returns>True if the attribute is defined on the specified member.</returns>
    public static object GetCustomAttributeFast(this MemberInfo member, Type attributeType)
    {
        Tuple<string, Type, Type> cacheKey =
            Tuple.Create(member.Name, member.DeclaringType, attributeType);
        object result = CachedPropertyAttributes.GetOrAdd(cacheKey, key =>
        {
            try { return Attribute.GetCustomAttribute(member, attributeType, true); }
            catch (Exception ex) { return ex; }
        });
        if (result is Exception exceptionResult)
            throw exceptionResult;
        return result;
    }
    #endregion Cached Attribute Retrieval
Alain
  • 26,663
  • 20
  • 114
  • 184