runtime: Nullability of JsonConverter.Write(T value) not correct
The method JsonConverter<T>.Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
has a non-nullable T value
but null
can be passed if the converter overrides the bool HandleNull {get}
method and returns true. That method was added in 5.0 so I believe there was miscommunication around the nullability there. The default is false
meaning null
is not passed and the serializer handles it.
API suggestion:
namespace System.Text.Json.Serialization
{
public abstract class JsonConverter<T> : System.Text.Json.Serialization.JsonConverter
{
- public abstract void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options);
+ public abstract void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options);
}
}
This is a compile-time breaking change for all custom converters (which are common) that are compiled in a nullable context.
Note the corresponding “T? Read()” method already supports nullability.
@stephentoub are we still making nullable changes in 6.0 like this?
UPDATE: per discussion, we will unannotate the value
parameter instead of making it nullable. This may prevent consumers from assuming the converter can always handle null
. Each derived type should specify the appropriate nullability on the value
parameter.
About this issue
- Original URL
- State: closed
- Created 3 years ago
- Comments: 19 (19 by maintainers)
Since the consumer scenario where someone calls the converter directly (Eirik’s example) may NullReference, that is a valid reason not to add the nullable annotation. When forwarding to a converter in that example, HandleNull should be called first to determine if it should call the converter or just write null for it, but since we can’t enforce that pattern, erroring on the side of safety seems better even though it goes against the mainstream nullability practices.
Also, not obvious, but if we don’t annotate nullable on the base class
JsonConverter<T>.Write(...)
it does allow a derived converter to add the nullable if it overridesAllowNull
and returnstrue
, so nullability should be (or can be made) correct for consumers if calling a concrete converter class which should be the common case.FWIW the canonical example of overriding HandleNull and returning true is a custom String converter that writes a null string reference an empty JSON string (instead of writing null or ignoring it). In any case, these scenarios should be rare.
To enumerate the options:
null
is handled, causing NullReference.JsonConverter<T>
. Although no nullability attributes are written to IL forJsonConverter<T>.Write()
, currently for Roslyn this appears is going to be the same as (1) where ! may be needed when calling the base class in order to prevent a warning\error. See sample below.In all cases, we should also update the existing xml doc for Write() to clear up the semantics and mention HandleNull.
Sample code
Ah, indeed. Here’s an illustration of the behavior. I believe the new reflection APIs would express the difference, but the editor isn’t presenting the oblivious state.
My understanding is that we want to always annotate for nullability even for the rare cases. E.g.
Activator.CreateInstance()
may return anull
object in rare cases so the return value is nullable.Since this is on an abstract base class, there are both author and consumer implications:
HandleNull
to return true, the annotation is not correct.HandleNull
(which is the common case), the annotation is not accurate sincevalue
cannot benull
.Write()
method directly, the annotation may not be correct if the converter supports returning null. The consumer should checkHandleNull
before calling the converter.@steveharter We have been treating nullable annotation mistakes as bugs; we don’t need to go through API Review. Instead, we are simply aggregating the changes made in documentation by adding them to dotnet/docs#21202.