Reputation: 51
I would like to pass a parameter to the Json converter at the time of deserialization. At the same time, I would like the converter to execute only for the properties indicated by the attribute.
public class Contract
{
[JsonConverter(typeof(MyJsonConverter))]
public string Property { get; set; }
}
string parameter = "value";
var jsonSerializerSettings = new JsonSerializerSettings
{
Converters = { new MyJsonConverter(parameter) },
};
var contract = JsonConvert.DeserializeObject<Contract>(json, jsonSerializerSettings);
public class MyJsonConverter : JsonConverter
{
private readonly string _parameter;
public MyJsonConverter(string parameter)
{
_parameter = parameter;
}
public override bool CanConvert(Type objectType)
{
//
}
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
// use _parameter here
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
//
}
}
I know that the JsonConverter attribute accepts parameters for the converter, but then I would have to add one parameter to the Contract class permanently.
[JsonConverter(typeof(MyJsonConverter), <parameters>)]
I would like the parameters to be dynamically provided at the time of deserialization - how do I achieve this?
Upvotes: 1
Views: 2149
Reputation: 116794
You can use StreamingContext.Context
from JsonSerializerSettings.Context
to pass data into a JsonConverter.
First, define the following interface and classes to cache data, keyed by System.Type
, inside a StreamingContext
:
public static class StreamingContextExtensions
{
public static StreamingContext AddTypeData(this StreamingContext context, Type type, object? data)
{
var c = context.Context;
IStreamingContextTypeDataDictionary dictionary;
if (context.Context == null)
dictionary = new StreamingContextTypeDataDictionary();
else if (context.Context is IStreamingContextTypeDataDictionary d)
dictionary = d;
else
throw new InvalidOperationException(string.Format("context.Context is already populated with {0}", context.Context));
dictionary.AddData(type, data);
return new StreamingContext(context.State, dictionary);
}
public static bool TryGetTypeData(this StreamingContext context, Type type, out object? data)
{
IStreamingContextTypeDataDictionary? dictionary = context.Context as IStreamingContextTypeDataDictionary;
if (dictionary == null)
{
data = null;
return false;
}
return dictionary.TryGetData(type, out data);
}
}
public interface IStreamingContextTypeDataDictionary
{
public void AddData(Type type, object? data);
public bool TryGetData(Type type, out object? data);
}
class StreamingContextTypeDataDictionary : IStreamingContextTypeDataDictionary
{
readonly Dictionary<Type, object?> dictionary = new ();
public void AddData(Type type, object? data) => dictionary.Add(type, data);
public bool TryGetData(Type type, out object? data) => dictionary.TryGetValue(type, out data);
}
Then rewrite MyConverter
as follows:
public class MyJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => objectType == typeof(string);
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
// Grab parameter from serializer.Context. Use some default value (here "") if not present.
var _parameter = serializer.Context.TryGetTypeData(typeof(MyJsonConverter), out var s) ? (string?)s : "";
// Use _parameter as required, e.g.
return _parameter + (string?)JToken.Load(reader);
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) =>
writer.WriteValue((string)value!);
}
And you will be able to deserialize as follows:
var _parameter = "my runtime parameter: ";
var settings = new JsonSerializerSettings
{
Context = new StreamingContext(StreamingContextStates.All)
.AddTypeData(typeof(MyJsonConverter), _parameter),
// Add any other required customizations,
};
var contract = JsonConvert.DeserializeObject<Contract>(json, settings);
Notes:
StreamingContext
is keyed by type so that multiple converters could access cached data inside without interfering with each other. The type used should be the converter type, not the property type.Demo fiddle #1 here.
Honestly though I don't recommend this design. StreamingContext
is unfamiliar to current .NET programmers (it's a holdover from binary serialization) and it feels completely surprising to use it to pass data deep down into some JsonConverter.ReadJson()
method.
As an alternative, you might consider creating a custom contract resolver that replaces the default MyJsonConverter
applied at compile time with a different instance that has the required parameters.
First, define the following contract resolver:
public class ConverterReplacingContractResolver : DefaultContractResolver
{
readonly Dictionary<(Type type, string name), JsonConverter?> replacements;
public ConverterReplacingContractResolver(IEnumerable<KeyValuePair<(Type type, string name), JsonConverter?>> replacements) =>
this.replacements = (replacements ?? throw new ArgumentNullException()).ToDictionary(r => r.Key, r => r.Value);
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
if (member.DeclaringType != null && replacements.TryGetValue((member.DeclaringType, member.Name), out var converter))
property.Converter = converter;
return property;
}
}
Then modify MyJsonConverter
so it has a default constructor with a default value for _parameter
:
public class MyJsonConverter : JsonConverter
{
private readonly string _parameter;
public MyJsonConverter() : this("") { }
public MyJsonConverter(string parameter) => this._parameter = parameter;
public override bool CanConvert(Type objectType) => objectType == typeof(string);
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) =>
_parameter + (string?)JToken.Load(reader);
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) =>
writer.WriteValue((string)value!);
}
And now you will be able to deserialize as follows:
var _parameter = "my runtime parameter: ";
var replacementsConverters = new KeyValuePair<(Type type, string name), JsonConverter?> []
{
new((typeof(Contract), nameof(Contract.Property)), new MyJsonConverter(_parameter)),
};
var resolver = new ConverterReplacingContractResolver(replacementsConverters)
{
// Add any other required customizations, e.g.
//NamingStrategy = new CamelCaseNamingStrategy()
};
var settings = new JsonSerializerSettings
{
ContractResolver = resolver,
// Add other settings as required,
};
var contract = JsonConvert.DeserializeObject<Contract>(json, settings);
Demo fiddle #2 here.
Upvotes: 2