Can System.Text.Json tolerate an unrecognized type discriminator during deserialization?

Summary

A deserialization failure occurred when System.Text.Json rejected an unknown type discriminator in a polymorphic payload. Instead of gracefully ignoring the unrecognized subtype, the serializer threw a NotSupportedException, preventing the object graph from being materialized.

Root Cause

The failure stems from how System.Text.Json enforces strict polymorphic type resolution:

  • The interface IBar is marked with [JsonPolymorphic(IgnoreUnrecognizedTypeDiscriminators = true)], but this attribute only applies when a discriminator is present but invalid, not when the serializer believes a discriminator is required.
  • Because IBar is an interface with no default concrete type, System.Text.Json requires a recognized discriminator to instantiate a subtype.
  • When the discriminator is unknown, the serializer cannot choose a fallback type, so it throws instead of returning null.

Why This Happens in Real Systems

Real-world JSON polymorphism often breaks because:

  • Producers evolve faster than consumers, adding new derived types.
  • Strict deserializers assume full schema knowledge, rejecting anything unknown.
  • Interfaces and abstract types lack a natural fallback, forcing the serializer to choose between correctness and tolerance.
  • Type discriminators are not standardized, so different services encode them differently.

Real-World Impact

This kind of failure can cause:

  • Hard crashes during message processing.
  • Partial data loss, because the entire object fails to deserialize.
  • Compatibility issues between microservices deployed at different versions.
  • Operational instability, especially when upstream systems introduce new message types.

Example or Code (if necessary and relevant)

Below is a minimal example of how the failure occurs and how a fallback can be implemented using a custom converter:

public class TolerantBarConverter : JsonConverter
{
    public override IBar Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        using var doc = JsonDocument.ParseValue(ref reader);
        if (!doc.RootElement.TryGetProperty("$type", out var typeProp))
            return null;

        var typeName = typeProp.GetString();
        return typeName switch
        {
            nameof(KnownBar) => doc.RootElement.Deserialize(options),
            _ => null
        };
    }

    public override void Write(Utf8JsonWriter writer, IBar value, JsonSerializerOptions options)
        => JsonSerializer.Serialize(writer, value, value.GetType(), options);
}

How Senior Engineers Fix It

Experienced engineers typically apply one of these strategies:

  • Implement a custom converter to gracefully handle unknown discriminators.
  • Provide a fallback type, such as UnknownBar, to preserve the payload for logging or later processing.
  • Avoid interface-based polymorphism in public contracts, using composition or union types instead.
  • Add versioning to message schemas, allowing consumers to ignore unknown fields safely.
  • Wrap deserialization in a tolerant envelope, capturing errors without failing the entire operation.

Why Juniors Miss It

Less experienced developers often overlook this issue because:

  • They assume attributes guarantee behavior, without understanding serializer internals.
  • They expect IgnoreUnrecognizedTypeDiscriminators to apply universally, even when no fallback type exists.
  • They underestimate how strict polymorphism is in System.Text.Json.
  • They rarely test forward-compatibility scenarios, focusing only on known types.
  • They assume interfaces behave like concrete classes during deserialization.

Leave a Comment