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
IBaris 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
IBaris 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.