-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Description
Internal json collection converters for instance assume JsonClassInfo.ElementInfo
is not null, however it can actually be null if the converter has been pre-aquired from another options instance.
Line 42 in 79ae74f
JsonClassInfo elementClassInfo = state.Current.JsonClassInfo.ElementClassInfo!; |
ElementInfo
returns null if ElementType
is null and ElementType
will only be filled under specific circumstances.
When we start at the point of our custom converter's Read() method (see below) we get the following steps to an NRE:
- pre-aquired JsonConverter.Read will initialize its own state (readstack) with among others a call to
options.GetOrAddClassForRootType(type)
- This will then do a resolve for the type and its converter in the constructor of
JsonClassInfo(type)
. - Converter, which is resolved back again to the custom converter here will have
ClassType.None
because all custom converters do. - State is now initialized with
ElementType = null
because it didn't fall into theClassType.Collection
arm - Call to OnTryRead commences with state that will never return anything but null for
JsonClassInfo.ElementInfo
- NRE thrown
using System;
using System.Collections.Immutable;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StjRepro
{
public enum Cases
{
One,
Two,
Three,
Four
}
class JsonImmutableArrayConverter<T> : JsonConverter<ImmutableArray<T>>
{
JsonConverter<ImmutableArray<T>> _originalConverter;
public JsonImmutableArrayConverter()
=> _originalConverter = (JsonConverter<ImmutableArray<T>>)new JsonSerializerOptions().GetConverter(typeof(ImmutableArray<T>));
public override ImmutableArray<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
// We're passing the current options and not the default options because we do want our value converters to work
// in this example, from strings to the enum 'Cases'.
=> _originalConverter.Read(ref reader, typeToConvert, options);
public override void Write(Utf8JsonWriter writer, ImmutableArray<T> value, JsonSerializerOptions options)
{
throw new NotSupportedException();
}
}
class JsonImmutableArrayConverter : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert)
=> typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(ImmutableArray<>);
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
=> (JsonConverter)Activator.CreateInstance(typeof(JsonImmutableArrayConverter<>).MakeGenericType(typeToConvert.GenericTypeArguments[0]));
}
class Program
{
static void Main(string[] args)
{
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonImmutableArrayConverter());
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
JsonSerializer.Deserialize<ImmutableArray<Cases>>(@"[""one"",""two"",""three"",""four""]", options);
}
}
}
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
at System.Text.Json.Serialization.Converters.IEnumerableDefaultConverter`2.GetElementConverter(JsonClassInfo elementClassInfo)
at System.Text.Json.Serialization.Converters.IEnumerableDefaultConverter`2.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, TCollection& value)
at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
at System.Text.Json.Serialization.JsonResumableConverter`1.Read(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options)
at StjRepro.JsonImmutableArrayConverter`1.Read(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options)
at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
at System.Text.Json.JsonSerializer.ReadCore[TValue](JsonConverter jsonConverter, Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
at System.Text.Json.JsonSerializer.ReadCore[TValue](Utf8JsonReader& reader, Type returnType, JsonSerializerOptions options)
at System.Text.Json.JsonSerializer.Deserialize[TValue](ReadOnlySpan`1 json, Type returnType, JsonSerializerOptions options)
at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)
at StjRepro.Program.Main(String[] args)
The reason for pulling out an 'original' converter like this is because some code paths in a custom converter should just be able to default to existing converters, merely wrapping over them.
The only way I see to hack around this without a proper fix is passing a specially crafted options instance into the framework converter that has this custom converter removed, it will resolve the correct JsonClassInfo
and includes the required custom value converters but in terms of perf (and usability) it seems far from ideal.
public override ImmutableArray<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var optionsDiff = new JsonSerializerOptions(options);
JsonConverter factory = null;
foreach (var converter in optionsDiff.Converters)
{
if (converter.GetType() == typeof(JsonImmutableArrayConverter))
factory = converter;
}
if (factory != null)
optionsDiff.Converters.Remove(factory);
// We're passing the current options and not the default options because we do want our value converters to work
// in this case from strings to the enum 'Cases'.
return _originalConverter.Read(ref reader, typeToConvert, optionsDiff);
}
If there is some other method by which to achieve what I want, I'd be glad to use it. In any case I think its good to add some documentation on how to call into the framework converters from a custom converter, this has been something I've wanted to do (and done to various degrees of success) multiple times now.
/cc @layomia