JamesNK/Newtonsoft.Json · error · JsonException

Serialization Callback '{1}' in type '{0}' must return void.

Error message

Serialization Callback '{1}' in type '{0}' must return void.

What it means

Serialization callback methods must return void. IsValidCallback checks method.ReturnType != typeof(void) and throws a JsonException (line 1332). The serializer ignores any return value, so a non-void return indicates a misdeclared hook.

Source

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

            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))
                {
                    throw new JsonException("Serialization Callback '{1}' in type '{0}' must have a single parameter of type '{2}'.".FormatWith(CultureInfo.InvariantCulture, GetClrTypeFullName(method.DeclaringType!), method, typeof(StreamingContext)));
                }
            }

            prevAttributeType = attributeType;

View on GitHub (pinned to 4f73e74372)

Solutions

  1. Change the method's return type to void.
  2. If using async work, perform synchronous work in the callback or move async logic elsewhere (do not return Task from a callback).

Example fix

// before
public class Model {
    [OnDeserialized]
    internal bool OnDeserialized(StreamingContext c) => true; // throws
}

// after
public class Model {
    [OnDeserialized]
    internal 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 (attrs.Any(a => m.IsDefined(a, false)) && m.ReturnType != typeof(void))
        throw new InvalidOperationException($"Callback method {m.Name} must return void.");
}

Type guard

static bool AllCallbacksReturnVoid(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.ReturnType == typeof(void));
}

Try / catch

try { JsonConvert.DeserializeObject<T>(json); }
catch (JsonException ex) when (ex.Message.Contains("must return void"))
{
    // change the callback return type to void
}

Prevention

When it happens

Trigger: An On* callback method declared with a return type (int, bool, Task, the model type, etc.) instead of void.

Common situations: Reusing an existing helper that returns a value as a callback, or declaring the callback as async Task (async void or Task both break the void requirement).

Related errors


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