JamesNK/Newtonsoft.Json · error · JsonException

Virtual Method '{0}' of type '{1}' cannot be marked with '{2

Error message

Virtual Method '{0}' of type '{1}' cannot be marked with '{2}' attribute.

What it means

Serialization callback attributes cannot be applied to virtual methods. IsValidCallback checks method.IsVirtual and throws a JsonException (line 1327). Virtual callbacks are rejected because a derived override could change the signature/behavior in ways the contract resolver cannot statically validate, breaking reliable serialization lifecycle hooks.

Source

Thrown at Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs:1327

        {
            if (!method.IsDefined(attributeType, false))
            {
                return false;
            }

            if (currentCallback != null)
            {
                throw new JsonException("Invalid attribute. Both '{0}' and '{1}' in type '{2}' have '{3}'.".FormatWith(CultureInfo.InvariantCulture, method, currentCallback, GetClrTypeFullName(method.DeclaringType!), attributeType));
            }

            if (prevAttributeType != null)
            {
                throw new JsonException("Invalid Callback. Method '{3}' in type '{2}' has both '{0}' and '{1}'.".FormatWith(CultureInfo.InvariantCulture, prevAttributeType, attributeType, GetClrTypeFullName(method.DeclaringType!), method));
            }

            if (method.IsVirtual)
            {
                throw new JsonException("Virtual Method '{0}' of type '{1}' cannot be marked with '{2}' attribute.".FormatWith(CultureInfo.InvariantCulture, method, GetClrTypeFullName(method.DeclaringType!), attributeType));
            }

            if (method.ReturnType != typeof(void))
            {
                throw new JsonException("Serialization Callback '{1}' in type '{0}' must return void.".FormatWith(CultureInfo.InvariantCulture, GetClrTypeFullName(method.DeclaringType!), method));
            }

            if (attributeType == typeof(OnErrorAttribute))
            {
                if (parameters == null || parameters.Length != 2 || parameters[0].ParameterType != typeof(StreamingContext) || parameters[1].ParameterType != typeof(ErrorContext))
                {
                    throw new JsonException("Serialization Error Callback '{1}' in type '{0}' must have two parameters of type '{2}' and '{3}'.".FormatWith(CultureInfo.InvariantCulture, GetClrTypeFullName(method.DeclaringType!), method, typeof(StreamingContext), typeof(ErrorContext)));
                }
            }
            else
            {
                if (parameters == null || parameters.Length != 1 || parameters[0].ParameterType != typeof(StreamingContext))
                {

View on GitHub (pinned to 4f73e74372)

Solutions

  1. Make the callback method non-virtual (remove 'virtual'/'override').
  2. If a derived type needs its own hook, give the derived type its own private non-virtual [OnDeserialized] method instead of overriding the base one.

Example fix

// before
public class Model {
    [OnDeserialized]
    protected virtual void OnDeserialized(StreamingContext c) {} // throws
}

// after
public class Model {
    [OnDeserialized]
    protected void OnDeserialized(StreamingContext c) {}
}
Defensive patterns

Strategy: validation

Validate before calling

var attrs = new[] { typeof(OnSerializingAttribute), typeof(OnSerializedAttribute), typeof(OnDeserializingAttribute), typeof(OnDeserializedAttribute), typeof(OnErrorAttribute) };
foreach (var m in typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
    if (m.IsVirtual && attrs.Any(a => m.IsDefined(a, false)))
        throw new InvalidOperationException($"Virtual method {m.Name} cannot carry a serialization callback attribute.");
}

Type guard

static bool NoVirtualCallbackMethods(Type t)
{
    var attrs = new[] { typeof(OnSerializingAttribute), typeof(OnSerializedAttribute), typeof(OnDeserializingAttribute), typeof(OnDeserializedAttribute), typeof(OnErrorAttribute) };
    return t.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
        .Where(m => attrs.Any(a => m.IsDefined(a, false)))
        .All(m => !m.IsVirtual);
}

Try / catch

try { JsonConvert.DeserializeObject<T>(json); }
catch (JsonException ex) when (ex.Message.Contains("cannot be marked with"))
{
    // make the callback method non-virtual
}

Prevention

When it happens

Trigger: Decorating a 'virtual' (or 'override') method with any of the On* callback attributes.

Common situations: Adding [OnDeserialized] to an override of a base virtual method, or marking the callback virtual so subclasses can extend it.

Related errors


AI-assisted analysis of JamesNK/Newtonsoft.Json@4f73e74372 (2026-08-07). Data as JSON: /api/errors/1c4fe41c4fe8d94d. Report an issue: GitHub.