-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
Background and motivation
While working on a client library for Elasticsearch, making heavy use of System.Text.Json, a common issue I have run into is the need to access some additional state inside custom converters. The Elasticsearch client has a settings class ElasticsearchClientSettings
which includes state used to infer values for certain types at runtime.
Currently, this forces an instance of each converter which requires access to the settings to be created in advance and added to the JsonSerializerOptions
(example). These converters can then accept an ElasticsearchClientSettings
instance via their constructor. Some of these instances may never be needed if the types they (de)serialize are not used by consumers of our library. Additionally, we have some converters which we code generate which makes creating such instances and adding them to the JsonSerializerOptions
more complicated.
I have come up with a rather nasty workaround where I register a converter purely to hold state, which can then be retrieved from the options to gain access to the settings.
internal sealed class ExtraSerializationData : JsonConverter<ExtraSerializationData>
{
public ExtraSerializationData(IElasticsearchClientSettings settings) => Settings = settings;
public IElasticsearchClientSettings Settings { get; }
public override ExtraSerializationData? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException();
public override void Write(Utf8JsonWriter writer, ExtraSerializationData value, JsonSerializerOptions options) => throw new NotImplementedException();
}
This can then be accessed in the Write
method of a custom converter:
public override void Write(Utf8JsonWriter writer, SortOptions value, JsonSerializerOptions options)
{
writer.WriteStartObject();
if (value.AdditionalPropertyName is IUrlParameter urlParameter)
{
var extraData = options.GetConverter(typeof(ExtraSerializationData)) as ExtraSerializationData;
var propertyName = urlParameter.GetString(extraData.Settings);
writer.WritePropertyName(propertyName);
}
else
{
throw new JsonException();
}
JsonSerializer.Serialize(writer, value.Variant, value.Variant.GetType(), options);
writer.WriteEndObject();
}
API Proposal
I would like to propose adding support to attach extra custom data (a property bag) to JsonSerializerOptions
, to be accessible inside the Read
and Write
methods of custom converters derived from JsonConveter<T>
by accessing the JsonSerializerOptions
.
public sealed class JsonSerializerOptions
{
public IReadOnlyDictionary<string, object> CustomData { get; set; } = new Dictionary<string, object>(0); // some cached default empty value
}
This adds a property to act as a property bag for user-provided data. I'm proposing this be exposed as IReadOnlyDictionary
to avoid new items being added/modified after the options become immutable. It could be IDictionary
to support scenarios where some converters need to add data for subsequent use, although that sounds risky. I imagine this should be initialised with a cached empty dictionary in cases where the user does not explicitly set this.
API Usage
Define options with custom data:
var options = new JsonSerializerOptions
{
CustomData = new Dictionary<string, object> {{ "settings", new Settings() }}
}
Access within a custom converter:
public override void Write(Utf8JsonWriter writer, SortOptions value, JsonSerializerOptions options)
{
writer.WriteStartObject();
if (value.AdditionalPropertyName is IUrlParameter urlParameter && options.CustomData.TryGetValue("settings", out var settings))
{
var propertyName = urlParameter.GetString(settings);
writer.WritePropertyName(propertyName);
}
else
{
throw new JsonException();
}
JsonSerializer.Serialize(writer, value.Variant, value.Variant.GetType(), options);
writer.WriteEndObject();
}
Alternative Designs
If the JsonSerializerOptions
were unsealed, that could also support my scenario. We could define a subclass of JsonSerializerOptions
with extra properties. Inside our converters that require that data, they could try casting to the derived type and access any necessary extra properties.
- public sealed class JsonSerializerOptions
+ public class JsonSerializerOptions
{
}
Risks
This could potentially be misused to hold mutable types which could cause thread-safety issues. Documentation could be added to guide the intended usage.