Deserializing nested dictionaries where keys (strings) are interned? - c#

suppose I have an existing concrete class that I want to deserialize some json into:
public class Jeff
{
[JsonProperty("string_to_int")]
public Dictionary<string, Dictionary<string, Dictionary<string, int>>> StringToInt;
}
Let's say the string keys at all levels of this data structure are from a finite set of words, and I want to string.Intern() all of them. Is there an easy way to intern all the string keys directly in my Deserialize<> call please?

You would like to use something like AutoInterningStringConverter from string Intern on serializer.Deserialize<T>() to automatically intern the dictionary keys as they are being deserialized, however Json.NET will not invoke a custom JsonConverter for dictionary keys[1].
Thus you will need to create a custom JsonConverter for all Dictionary<string, TValue> types which automatically interns the keys. The following does this:
public class AutoInterningDictionaryKeyConverter : JsonConverter
{
const int DefaultMaxToIntern = 64;
const int MaxLengthToIntern = 2048; // Modify as required
int maxToIntern;
public AutoInterningDictionaryKeyConverter() : this(DefaultMaxToIntern) { }
public AutoInterningDictionaryKeyConverter(int maxToIntern) => this.maxToIntern = maxToIntern;
bool CanConvert(Type objectType, [System.Diagnostics.CodeAnalysis.NotNullWhen(returnValue: true)] out Type? valueType)
{
if (typeof(IDictionary).IsAssignableFrom(objectType)
&& (objectType.GetDictionaryKeyValueType() is var keyValueTypes && keyValueTypes is not null)
&& keyValueTypes[0] == typeof(string)
&& objectType.GetConstructor(Type.EmptyTypes) is not null)
{
valueType = keyValueTypes[1];
return true;
}
valueType = null;
return false;
}
public override bool CanConvert(Type objectType) => CanConvert(objectType, out var _);
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.StartObject)
throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
// Here we take advantage of the fact that Dictionary<TKey, TValue> implements IDictionary.
// Recall that, in CanConvert, we checked to make sure the dictionary had a public parameterless constructor, so DefaultCreator won't be null
var dictionary = existingValue as IDictionary ?? (IDictionary)(serializer.ContractResolver.ResolveContract(objectType).DefaultCreator!());
var keyValueTypes = objectType.GetDictionaryKeyValueType().ThrowOnNull();
while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndObject)
{
var name = (string)reader.AssertTokenType(JsonToken.PropertyName).Value.ThrowOnNull();
if (String.IsInterned(name) is var s && s is not null)
name = s;
else if (name.Length <= MaxLengthToIntern)
{
if (Interlocked.Decrement(ref maxToIntern) >= 0)
name = string.Intern(name);
else
Volatile.Write(ref maxToIntern, 0); // Don't let maxToIntern underflow int.MinValue (extremely unlikely but still not technically impossible.
}
dictionary.Add(name, serializer.Deserialize(reader.ReadToContentAndAssert(), keyValueTypes[1]));
}
return dictionary;
}
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => throw new NotImplementedException();
}
public static partial class JsonExtensions
{
public static JsonReader AssertTokenType(this JsonReader reader, JsonToken tokenType) =>
reader.TokenType == tokenType ? reader : throw new JsonSerializationException(string.Format("Unexpected token {0}, expected {1}", reader.TokenType, tokenType));
public static JsonReader ReadToContentAndAssert(this JsonReader reader) =>
reader.ReadAndAssert().MoveToContentAndAssert();
public static JsonReader MoveToContentAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (reader.TokenType == JsonToken.None) // Skip past beginning of stream.
reader.ReadAndAssert();
while (reader.TokenType == JsonToken.Comment) // Skip past comments.
reader.ReadAndAssert();
return reader;
}
public static JsonReader ReadAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (!reader.Read())
throw new JsonReaderException("Unexpected end of JSON stream.");
return reader;
}
}
public static class TypeExtensions
{
public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
=> (type ?? throw new ArgumentNullException()).IsInterface ? new[] { type }.Concat(type.GetInterfaces()) : type.GetInterfaces();
public static IEnumerable<Type []> GetDictionaryKeyValueTypes(this Type type)
=> type.GetInterfacesAndSelf().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IDictionary<,>)).Select(t => t.GetGenericArguments());
public static Type []? GetDictionaryKeyValueType(this Type type)
{
var types = type.GetDictionaryKeyValueTypes().ToList();
return types.Count == 1 ? types[0] : null;
}
public static T ThrowOnNull<T>(this T? value) where T : class => value ?? throw new ArgumentNullException();
}
Notes:
To prevent unexpected or malicious JSON from swamping your interned string pool and degrading the performance of your entire program, the converter will only add a limited number number of strings to the pool. It will also only add strings shorter than some specified length.
You can modify the limits as required.
The converter works for read/write dictionaries that implement both IDictionary and IDictionary<string, TValue> for some TValue. It would need to be enhanced to support immutable dictionaries.
Demo fiddle here.
[1] For confirmation, see this comment by JamesNK to Dictionary conversion for complex key-types is really buggy, defaults to ToString() output #2440:
JsonConverter isn't used for dictionary keys because they're strings, not JSON. That is why ToString or a TypeConverter is used.

Related

JSON.NET overwrites values of dictionary by populating [duplicate]

I would like to populate the objects contained within a Dictionary from a JSON file while preserving the object references themselves.
Json.net documentation on PreserveReferencesHandling clearly state that it will not work in case a type implements System.Runtime.Serialization.ISerializable:
Specifies reference handling options for the
Newtonsoft.Json.JsonSerializer. Note that references cannot be
preserved when a value is set via a non-default constructor such as
types that implement System.Runtime.Serialization.ISerializable.
Here is my failing code:
class Model
{
public int Val { get; set; } = 123;
}
...
var model = new Model();
var to_serialize = new Dictionary<int, Model> { { 0, model } }; // works ok with list<Model>
// serialize
var jsonString = JsonConvert.SerializeObject(to_serialize, Formatting.Indented);
var jsonSerializerSettings = new JsonSerializerSettings();
jsonSerializerSettings.MissingMemberHandling = MissingMemberHandling.Ignore;
jsonSerializerSettings.PreserveReferencesHandling = PreserveReferencesHandling.All; // does not work for ISerializable
Assert.AreSame(to_serialize[0], model); // ok!
JsonConvert.PopulateObject(
value: jsonString,
target: to_serialize,
settings: jsonSerializerSettings
);
Assert.AreSame(to_serialize[0], model); // not ok... works ok with list<Model>
My main requirement is that when calling PopulateObject(), the constructor of the Model class will not be invoked. Instead, only its internal field will be updated with the value from the JSON.
In my real case, the Model class contains other values which are not in the JSON and which I don't want to lose:
[JsonObject(MemberSerialization.OptIn)]
class Model
{
[JsonProperty(PropertyName = "val_prop")]
public int Val { get; set; } = 123;
// not in the json file, would like this field to maintain the value
// it had prior to PopulateObject()
public int OtherVal { get; set; } = 456;
}
Is there a way to make this work?
Your problem is similar to the one from JsonSerializer.CreateDefault().Populate(..) resets my values: you would like to populate a preexisting collection, specifically a Dictionary<int, T> for some T, and populate the preexisting values. Unfortunately, in the case of a dictionary, Json.NET will replace the values rather than populate them, as can be seen in JsonSerializerInternalReader.PopulateDictionary() which simply deserializes the value to the appropriate type, and sets it the dictionary.
To work around this limitation, you can create a custom JsonConverter for Dictionary<TKey, TValue> when TKey is a primitive type and TValue is a complex type which merges the incoming JSON key/value pairs onto the preexisting dictionary. The following converter does the trick:
public class DictionaryMergeConverter : JsonConverter
{
static readonly IContractResolver defaultResolver = JsonSerializer.CreateDefault().ContractResolver;
readonly IContractResolver resolver = defaultResolver;
public override bool CanConvert(Type objectType)
{
var keyValueTypes = objectType.GetDictionaryKeyValueType();
if (keyValueTypes == null)
return false;
var keyContract = resolver.ResolveContract(keyValueTypes[0]);
if (!(keyContract is JsonPrimitiveContract))
return false;
var contract = resolver.ResolveContract(keyValueTypes[1]);
return contract is JsonContainerContract;
// Also possibly check whether keyValueTypes[1] is a read-only collection or dictionary.
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.StartObject)
throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
IDictionary dictionary = existingValue as IDictionary ?? (IDictionary)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
var keyValueTypes = objectType.GetDictionaryKeyValueType();
while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndObject)
{
switch (reader.TokenType)
{
case JsonToken.PropertyName:
var name = (string)reader.Value;
reader.ReadToContentAndAssert();
// TODO: DateTime keys and enums with overridden names.
var key = (keyValueTypes[0] == typeof(string) ? (object)name : Convert.ChangeType(name, keyValueTypes[0], serializer.Culture));
var value = dictionary.Contains(key) ? dictionary[key] : null;
// TODO:
// - JsonConverter active for valueType, either in contract or in serializer.Converters
// - NullValueHandling, ObjectCreationHandling, PreserveReferencesHandling,
if (value == null)
{
value = serializer.Deserialize(reader, keyValueTypes[1]);
}
else
{
serializer.Populate(reader, value);
}
dictionary[key] = value;
break;
default:
throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
}
}
return dictionary;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); }
}
public static partial class JsonExtensions
{
public static JsonReader ReadToContentAndAssert(this JsonReader reader)
{
return reader.ReadAndAssert().MoveToContentAndAssert();
}
public static JsonReader MoveToContentAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (reader.TokenType == JsonToken.None) // Skip past beginning of stream.
reader.ReadAndAssert();
while (reader.TokenType == JsonToken.Comment) // Skip past comments.
reader.ReadAndAssert();
return reader;
}
public static JsonReader ReadAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (!reader.Read())
throw new JsonReaderException("Unexpected end of JSON stream.");
return reader;
}
}
public static class TypeExtensions
{
public static IEnumerable<Type> BaseTypesAndSelf(this Type type)
{
while (type != null)
{
yield return type;
type = type.BaseType;
}
}
public static Type[] GetDictionaryKeyValueType(this Type type)
{
return type.BaseTypesAndSelf().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>)).Select(t => t.GetGenericArguments()).FirstOrDefault();
}
}
Having done so, you will encounter a secondary issue: Json.NET will never use a custom converter to populate the root object. To work around this you will need to call JsonConverter.ReadJson() directly, from some utility method:
public static partial class JsonExtensions
{
public static void PopulateObjectWithConverter(string value, object target, JsonSerializerSettings settings)
{
if (target == null || value == null)
throw new ArgumentNullException();
var serializer = JsonSerializer.CreateDefault(settings);
var converter = serializer.Converters.Where(c => c.CanConvert(target.GetType()) && c.CanRead).FirstOrDefault() ?? serializer.ContractResolver.ResolveContract(target.GetType()).Converter;
using (var jsonReader = new JsonTextReader(new StringReader(value)))
{
if (converter == null)
serializer.Populate(jsonReader, target);
else
{
jsonReader.MoveToContentAndAssert();
var newtarget = converter.ReadJson(jsonReader, target.GetType(), target, serializer);
if (newtarget != target)
throw new JsonException(string.Format("Converter {0} allocated a new object rather than populating the existing object {1}.", converter, value));
}
}
}
}
You will now be able to populate your dictionary as follows:
var jsonString = JsonConvert.SerializeObject(to_serialize, Formatting.Indented);
var settings = new JsonSerializerSettings
{
Converters = { new DictionaryMergeConverter() },
};
JsonExtensions.PopulateObjectWithConverter(jsonString, to_serialize, settings);
Notes:
PreserveReferencesHandling has no impact on whether dictionary values are populated or replaced. Instead this setting controls whether a serialization graph with multiple references to the same object will maintain its reference topology when round-tripped.
In your question you wrote // works ok with list<Model> but in fact this is not correct. When a List<T> is populated the new values are appended to the list, so Assert.AreSame(to_serialize[0], model); passes purely by luck. If you had additionally asserted Assert.AreSame(1, to_serialize.Count) it would have failed.
While the converter will work for primitive keys such as string and int it may not work for key types that require JSON-specific conversion such as enum or DateTime.
The converter is currently only implemented for Dictionary<TKey, TValue> and takes advantage of the fact that this type implements the non-generic IDictionary interface. It could be extended to other dictionary types such as SortedDictionary<TKey,TValue> if required.
Demo fiddle here.

Json.net - How to preserve dictionary value references when populating a dictionary?

I would like to populate the objects contained within a Dictionary from a JSON file while preserving the object references themselves.
Json.net documentation on PreserveReferencesHandling clearly state that it will not work in case a type implements System.Runtime.Serialization.ISerializable:
Specifies reference handling options for the
Newtonsoft.Json.JsonSerializer. Note that references cannot be
preserved when a value is set via a non-default constructor such as
types that implement System.Runtime.Serialization.ISerializable.
Here is my failing code:
class Model
{
public int Val { get; set; } = 123;
}
...
var model = new Model();
var to_serialize = new Dictionary<int, Model> { { 0, model } }; // works ok with list<Model>
// serialize
var jsonString = JsonConvert.SerializeObject(to_serialize, Formatting.Indented);
var jsonSerializerSettings = new JsonSerializerSettings();
jsonSerializerSettings.MissingMemberHandling = MissingMemberHandling.Ignore;
jsonSerializerSettings.PreserveReferencesHandling = PreserveReferencesHandling.All; // does not work for ISerializable
Assert.AreSame(to_serialize[0], model); // ok!
JsonConvert.PopulateObject(
value: jsonString,
target: to_serialize,
settings: jsonSerializerSettings
);
Assert.AreSame(to_serialize[0], model); // not ok... works ok with list<Model>
My main requirement is that when calling PopulateObject(), the constructor of the Model class will not be invoked. Instead, only its internal field will be updated with the value from the JSON.
In my real case, the Model class contains other values which are not in the JSON and which I don't want to lose:
[JsonObject(MemberSerialization.OptIn)]
class Model
{
[JsonProperty(PropertyName = "val_prop")]
public int Val { get; set; } = 123;
// not in the json file, would like this field to maintain the value
// it had prior to PopulateObject()
public int OtherVal { get; set; } = 456;
}
Is there a way to make this work?
Your problem is similar to the one from JsonSerializer.CreateDefault().Populate(..) resets my values: you would like to populate a preexisting collection, specifically a Dictionary<int, T> for some T, and populate the preexisting values. Unfortunately, in the case of a dictionary, Json.NET will replace the values rather than populate them, as can be seen in JsonSerializerInternalReader.PopulateDictionary() which simply deserializes the value to the appropriate type, and sets it the dictionary.
To work around this limitation, you can create a custom JsonConverter for Dictionary<TKey, TValue> when TKey is a primitive type and TValue is a complex type which merges the incoming JSON key/value pairs onto the preexisting dictionary. The following converter does the trick:
public class DictionaryMergeConverter : JsonConverter
{
static readonly IContractResolver defaultResolver = JsonSerializer.CreateDefault().ContractResolver;
readonly IContractResolver resolver = defaultResolver;
public override bool CanConvert(Type objectType)
{
var keyValueTypes = objectType.GetDictionaryKeyValueType();
if (keyValueTypes == null)
return false;
var keyContract = resolver.ResolveContract(keyValueTypes[0]);
if (!(keyContract is JsonPrimitiveContract))
return false;
var contract = resolver.ResolveContract(keyValueTypes[1]);
return contract is JsonContainerContract;
// Also possibly check whether keyValueTypes[1] is a read-only collection or dictionary.
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.StartObject)
throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
IDictionary dictionary = existingValue as IDictionary ?? (IDictionary)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
var keyValueTypes = objectType.GetDictionaryKeyValueType();
while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndObject)
{
switch (reader.TokenType)
{
case JsonToken.PropertyName:
var name = (string)reader.Value;
reader.ReadToContentAndAssert();
// TODO: DateTime keys and enums with overridden names.
var key = (keyValueTypes[0] == typeof(string) ? (object)name : Convert.ChangeType(name, keyValueTypes[0], serializer.Culture));
var value = dictionary.Contains(key) ? dictionary[key] : null;
// TODO:
// - JsonConverter active for valueType, either in contract or in serializer.Converters
// - NullValueHandling, ObjectCreationHandling, PreserveReferencesHandling,
if (value == null)
{
value = serializer.Deserialize(reader, keyValueTypes[1]);
}
else
{
serializer.Populate(reader, value);
}
dictionary[key] = value;
break;
default:
throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
}
}
return dictionary;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); }
}
public static partial class JsonExtensions
{
public static JsonReader ReadToContentAndAssert(this JsonReader reader)
{
return reader.ReadAndAssert().MoveToContentAndAssert();
}
public static JsonReader MoveToContentAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (reader.TokenType == JsonToken.None) // Skip past beginning of stream.
reader.ReadAndAssert();
while (reader.TokenType == JsonToken.Comment) // Skip past comments.
reader.ReadAndAssert();
return reader;
}
public static JsonReader ReadAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (!reader.Read())
throw new JsonReaderException("Unexpected end of JSON stream.");
return reader;
}
}
public static class TypeExtensions
{
public static IEnumerable<Type> BaseTypesAndSelf(this Type type)
{
while (type != null)
{
yield return type;
type = type.BaseType;
}
}
public static Type[] GetDictionaryKeyValueType(this Type type)
{
return type.BaseTypesAndSelf().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>)).Select(t => t.GetGenericArguments()).FirstOrDefault();
}
}
Having done so, you will encounter a secondary issue: Json.NET will never use a custom converter to populate the root object. To work around this you will need to call JsonConverter.ReadJson() directly, from some utility method:
public static partial class JsonExtensions
{
public static void PopulateObjectWithConverter(string value, object target, JsonSerializerSettings settings)
{
if (target == null || value == null)
throw new ArgumentNullException();
var serializer = JsonSerializer.CreateDefault(settings);
var converter = serializer.Converters.Where(c => c.CanConvert(target.GetType()) && c.CanRead).FirstOrDefault() ?? serializer.ContractResolver.ResolveContract(target.GetType()).Converter;
using (var jsonReader = new JsonTextReader(new StringReader(value)))
{
if (converter == null)
serializer.Populate(jsonReader, target);
else
{
jsonReader.MoveToContentAndAssert();
var newtarget = converter.ReadJson(jsonReader, target.GetType(), target, serializer);
if (newtarget != target)
throw new JsonException(string.Format("Converter {0} allocated a new object rather than populating the existing object {1}.", converter, value));
}
}
}
}
You will now be able to populate your dictionary as follows:
var jsonString = JsonConvert.SerializeObject(to_serialize, Formatting.Indented);
var settings = new JsonSerializerSettings
{
Converters = { new DictionaryMergeConverter() },
};
JsonExtensions.PopulateObjectWithConverter(jsonString, to_serialize, settings);
Notes:
PreserveReferencesHandling has no impact on whether dictionary values are populated or replaced. Instead this setting controls whether a serialization graph with multiple references to the same object will maintain its reference topology when round-tripped.
In your question you wrote // works ok with list<Model> but in fact this is not correct. When a List<T> is populated the new values are appended to the list, so Assert.AreSame(to_serialize[0], model); passes purely by luck. If you had additionally asserted Assert.AreSame(1, to_serialize.Count) it would have failed.
While the converter will work for primitive keys such as string and int it may not work for key types that require JSON-specific conversion such as enum or DateTime.
The converter is currently only implemented for Dictionary<TKey, TValue> and takes advantage of the fact that this type implements the non-generic IDictionary interface. It could be extended to other dictionary types such as SortedDictionary<TKey,TValue> if required.
Demo fiddle here.

How to ignore specific dictionary key in Json.NET deserialization?

How can one deserialize the following JSON
{
"result" : {
"master" : [
["one", "two"],
["three", "four"],
["five", "six", "seven"],
],
"blaster" : [
["ein", "zwei"],
["drei", "vier"]
],
"surprise" : "nonsense-nonsense-nonsense"
}
}
into the following data structure
class ResultView
{
public Dictionary<string, string[][]> Result { get; set; }
}
with Json.NET?
It has to be dictionary because key names such as 'master' and 'blaster' are unknown at the time of compilation. What is known is that they always point to an array of arrays of strings. The problem is that key 'surprise', whose name is known and always the same, points to something that cannot be interpreted as string[][], and this leads to exception in Json.NET.
Is there any way to make Json.NET ignore specific dictionary key?
You can introduce a custom generic JsonConverter for IDictionary<string, TValue> that filters out invalid dictionary values (i.e. those that cannot be deserialized successfully to the dictionary value type):
public class TolerantDictionaryItemConverter<TDictionary, TValue> : JsonConverter where TDictionary : IDictionary<string, TValue>
{
public override bool CanConvert(Type objectType)
{
return typeof(TDictionary).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type dictionaryType, object existingValue, JsonSerializer serializer)
{
// Get contract information
var contract = serializer.ContractResolver.ResolveContract(dictionaryType) as JsonDictionaryContract;
if (contract == null)
throw new JsonSerializationException(string.Format("Invalid JsonDictionaryContract for {0}", dictionaryType));
if (contract.DictionaryKeyType != typeof(string))
throw new JsonSerializationException(string.Format("Key type {0} not supported", dictionaryType));
var itemContract = serializer.ContractResolver.ResolveContract(contract.DictionaryValueType);
// Process the first token
var tokenType = reader.SkipComments().TokenType;
if (tokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.StartObject)
throw new JsonSerializationException(string.Format("Expected {0}, encountered {1} at path {2}", JsonToken.StartArray, reader.TokenType, reader.Path));
// Allocate the dictionary
var dictionary = existingValue as IDictionary<string, TValue> ?? (IDictionary<string, TValue>) contract.DefaultCreator();
// Process the collection items
while (reader.Read())
{
if (reader.TokenType == JsonToken.EndObject)
{
return dictionary;
}
else if (reader.TokenType == JsonToken.PropertyName)
{
var key = (string)reader.Value;
reader.ReadSkipCommentsAndAssert();
// For performance, skip tokens we can easily determine cannot be deserialized to itemContract
if (itemContract.QuickRejectStartToken(reader.TokenType))
{
System.Diagnostics.Debug.WriteLine(string.Format("value for {0} skipped", key));
reader.Skip();
}
else
{
// What we want to do is to distinguish between JSON files that are not WELL-FORMED
// (e.g. truncated) and that are not VALID (cannot be deserialized to the current item type).
// An exception must still be thrown for an ill-formed file.
// Thus we first load into a JToken, then deserialize.
var token = JToken.Load(reader);
try
{
var value = serializer.Deserialize<TValue>(token.CreateReader());
dictionary.Add(key, value);
}
catch (Exception)
{
System.Diagnostics.Debug.WriteLine(string.Format("value for {0} skipped", key));
}
}
}
else if (reader.TokenType == JsonToken.Comment)
{
continue;
}
else
{
throw new JsonSerializationException(string.Format("Unexpected token type {0} object at path {1}.", reader.TokenType, reader.Path));
}
}
// Should not come here.
throw new JsonSerializationException("Unclosed object at path: " + reader.Path);
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
public static partial class JsonExtensions
{
public static JsonReader SkipComments(this JsonReader reader)
{
while (reader.TokenType == JsonToken.Comment && reader.Read())
;
return reader;
}
public static void ReadSkipCommentsAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
while (reader.Read())
{
if (reader.TokenType != JsonToken.Comment)
return;
}
new JsonReaderException(string.Format("Unexpected end at path {0}", reader.Path));
}
internal static bool QuickRejectStartToken(this JsonContract contract, JsonToken token)
{
if (contract is JsonLinqContract)
return false;
switch (token)
{
case JsonToken.None:
return true;
case JsonToken.StartObject:
return !(contract is JsonContainerContract) || contract is JsonArrayContract; // reject if not dictionary or object
case JsonToken.StartArray:
return !(contract is JsonArrayContract); // reject if not array
case JsonToken.Null:
return contract.CreatedType.IsValueType && Nullable.GetUnderlyingType(contract.UnderlyingType) == null;
// Primitives
case JsonToken.Integer:
case JsonToken.Float:
case JsonToken.String:
case JsonToken.Boolean:
case JsonToken.Undefined:
case JsonToken.Date:
case JsonToken.Bytes:
return !(contract is JsonPrimitiveContract); // reject if not primitive.
default:
return false;
}
}
}
Then you can add it to settings as follows:
var settings = new JsonSerializerSettings
{
Converters = { new TolerantDictionaryItemConverter<IDictionary<string, TValue>, TValue>() },
};
var root = JsonConvert.DeserializeObject<ResultView>(json, settings);
Or add it directly to ResultView with JsonConverterAttribute:
class ResultView
{
[JsonConverter(typeof(TolerantDictionaryItemConverter<IDictionary<string, string[][]>, string[][]>))]
public Dictionary<string, string[][]> Result { get; set; }
}
Notes:
I wrote the converter in a general way to handle any type of dictionary value including primitives such as int or DateTime as well as arrays or objects.
While a JSON file with invalid dictionary values (ones that cannot be deserialized to the dictionary value type) should be deserializable, an ill-formed JSON file (e.g. one that is truncated) should still result in an exception being thrown.
The converter handles this by first loading the value into a JToken then attempting to deserialize the token. If the file is ill-formed, JToken.Load(reader) will throw an exception, which is intentionally not caught.
Json.NET's exception handling is reported to be "very flaky" (see e.g. Issue #1580: Regression from Json.NET v6: cannot skip an invalid object value type in an array via exception handling) so I did not rely on it to skip invalid dictionary values.
I'm not 100% sure I got all cases of comment handling correct. So that may need additional testing.
Working sample .Net fiddle here.
I think you could ignore exceptions like this:
ResultView result = JsonConvert.DeserializeObject<ResultView>(jsonString,
new JsonSerializerSettings
{
Error = delegate (object sender, Newtonsoft.Json.Serialization.ErrorEventArgs args)
{
// System.Diagnostics.Debug.WriteLine(args.ErrorContext.Error.Message);
args.ErrorContext.Handled = true;
}
}
);
args.ErrorContext.Error.Message would contain the actual error message.
args.ErrorContext.Handled = true; will tell Json.Net to proceed.

json.net serialize derived as generic base

(this issue stems from trying to serialize/deserialize LikeType classes to JSON - https://github.com/kleinwareio/LikeType)
I have:
public abstract class LikeType<T>
{
public T Value;
// ....
// how to tell json.net to serialize/deserialize classes deriving
// from this like it would T ???
}
public class Name : LikeType<string> {
public Name(string s) : base(s) { }
// does not add any properties
}
void test()
{
var name = new Name("john");
var jobj = new JObject();
try
{
jobj.Add("key", new JObject(name));
}
catch (Exception e)
{
!Exeption !
e = {System.ArgumentException: Could not determine JSON object type for type Name. at Newtonsoft.Json.Linq.JValue.GetValueType(Nullable`1 current, Object value) at Newtonsoft.Json.Linq.JContainer.CreateFromContent(Object content)
}
}
How can I specify that all classes deriving from LikeType<T> will be serialized/ deserialized to JSON with Json.Net in the same way T would?
(in this case, Json.Net should serialize/deserialize Name in the same way it would a string)
I believe you want to "forward" LikeType<T> serialization, treating this like an invisible wrapper type. This assumption is crucial to my solution.
I'd suggest using JsonConverter implementation to do that. There is a very similar post here: Json.NET - Serialize generic type wrapper without property name
I've adapted the example to your case. This is the adapted approach:
class LikeTypeConverter : JsonConverter
{
static Type GetValueType(Type objectType)
{
return objectType
.BaseTypesAndSelf()
.Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(LikeType<>))
.Select(t => t.GetGenericArguments()[0])
.FirstOrDefault();
}
public override bool CanConvert(Type objectType)
{
return GetValueType(objectType) != null;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// You need to decide whether a null JSON token results in a null LikeType<T> or
// an allocated LikeType<T> with a null Value.
if (reader.SkipComments().TokenType == JsonToken.Null)
return null;
var valueType = GetValueType(objectType);
var value = serializer.Deserialize(reader, valueType);
// Here we assume that every subclass of LikeType<T> has a constructor with a single argument, of type T.
return Activator.CreateInstance(objectType, value);
}
const string ValuePropertyName = "Value";// nameof(LikeType<object>.Value); // in C#6+
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(value.GetType());
var valueProperty = contract.Properties.Single(p => p.UnderlyingName == ValuePropertyName);
serializer.Serialize(writer, valueProperty.ValueProvider.GetValue(value));
}
}
public static partial class JsonExtensions
{
public static JsonReader SkipComments(this JsonReader reader)
{
while (reader.TokenType == JsonToken.Comment && reader.Read())
{
}
return reader;
}
}
public static class TypeExtensions
{
public static IEnumerable<Type> BaseTypesAndSelf(this Type type)
{
while (type != null)
{
yield return type;
type = type.BaseType;
}
}
}
You can use this as an attribute on LikeType<T> declaration if you want to include this in your library:
[JsonConverter(typeof(LikeTypeConverter))]
public abstract class LikeType<T> { ... }
Or you can use the converter when necessary, modifying JsonSerializerSettings.Converters collection:
var settings = new JsonSerializerSettings
{
Converters = { new LikeTypeConverter() },
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
var result = JsonConvert.SerializeObject(myObject, Formatting.Indented, settings);
I've also created a working dotnetfiddle sample for demonstration (also adapting the one from linked post).
One way of controlling what most serializers serialize is using the Serializable attribute and implement the ISerializable interface.

JsonConverter with Interface

I have an object which comes from the client and get deserialized from the Web Api 2 automatically.
Now I have a problem with one property of my model. This property "CurrentField" is of Type IField and there are 2 different Implementations of this interface.
This is my model (just a dummy)
public class MyTest
{
public IField CurrentField {get;set;}
}
public interface IField{
string Name {get;set;}
}
public Field1 : IField{
public string Name {get;set;}
public int MyValue {get;set;}
}
public Field2 : IField{
public string Name {get;set;}
public string MyStringValue {get;set;}
}
I tried to create a custom JsonConverter to find out of what type my object from the client is (Field1 or Field2) but I just don't know how.
My Converter gets called and I can see the object when I call
var obj = JObject.load(reader);
but how can I find out what type it is? I can't do something like
if(obj is Field1) ...
this is the method where I should check this right?
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
How to automatically select a concrete type when deserializing an interface using Json.NET
The easiest way to solve your problem is to serialize and deserialize your JSON (on both the client and server sides) with TypeNameHandling = TypeNameHandling.Auto. If you do, your JSON will include the actual type serialized for an IFIeld property, like so:
{
"CurrentField": {
"$type": "MyNamespace.Field2, MyAssembly",
"Name": "name",
"MyStringValue": "my string value"
}
}
However, note this caution from the Newtonsoft docs:
TypeNameHandling should be used with caution when your application deserializes JSON from an external source. Incoming types should be validated with a custom SerializationBinder when deserializing with a value other than None.
For a discussion of why this may be necessary, see TypeNameHandling caution in Newtonsoft Json, How to configure Json.NET to create a vulnerable web API, and Alvaro Muñoz & Oleksandr Mirosh's blackhat paper https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-JSON-Attacks-wp.pdf
If for whatever reason you cannot change what the server outputs, you can create a JsonConverter that loads the JSON into a JObject and checks to see what fields are actually present, then searches through possible concrete types to find one with the same properties:
public class JsonDerivedTypeConverer<T> : JsonConverter
{
public JsonDerivedTypeConverer() { }
public JsonDerivedTypeConverer(params Type[] types)
{
this.DerivedTypes = types;
}
readonly HashSet<Type> derivedTypes = new HashSet<Type>();
public IEnumerable<Type> DerivedTypes
{
get
{
return derivedTypes.ToArray();
}
set
{
if (value == null)
throw new ArgumentNullException();
derivedTypes.Clear();
if (value != null)
derivedTypes.UnionWith(value);
}
}
JsonObjectContract FindContract(JObject obj, JsonSerializer serializer)
{
List<JsonObjectContract> bestContracts = new List<JsonObjectContract>();
foreach (var type in derivedTypes)
{
if (type.IsAbstract)
continue;
var contract = serializer.ContractResolver.ResolveContract(type) as JsonObjectContract;
if (contract == null)
continue;
if (obj.Properties().Select(p => p.Name).Any(n => contract.Properties.GetClosestMatchProperty(n) == null))
continue;
if (bestContracts.Count == 0 || bestContracts[0].Properties.Count > contract.Properties.Count)
{
bestContracts.Clear();
bestContracts.Add(contract);
}
else if (contract.Properties.Count == bestContracts[0].Properties.Count)
{
bestContracts.Add(contract);
}
}
return bestContracts.Single();
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(T);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var obj = JObject.Load(reader); // Throws an exception if the current token is not an object.
var contract = FindContract(obj, serializer);
if (contract == null)
throw new JsonSerializationException("no contract found for " + obj.ToString());
if (existingValue == null || !contract.UnderlyingType.IsAssignableFrom(existingValue.GetType()))
existingValue = contract.DefaultCreator();
using (var sr = obj.CreateReader())
{
serializer.Populate(sr, existingValue);
}
return existingValue;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Then you can apply that as a converter to IField:
[JsonConverter(typeof(JsonDerivedTypeConverer<IField>), new object [] { new Type [] { typeof(Field1), typeof(Field2) } })]
public interface IField
{
string Name { get; set; }
}
Note that this solution is a little fragile. If the server omits the MyStringValue or MyValue fields (because they have default value and DefaultValueHandling = DefaultValueHandling.Ignore, for example) then the converter won't know which type to create and will throw an exception. Similarly, if two concrete types implementing IField have the same property names, differing only in type, the converter will throw an exception. Using TypeNameHandling.Auto avoids these potential problems.
Update
The following version checks to see if the "$type" parameter is present, and if TypeNameHandling != TypeNameHandling.None, falls back on default serialization. It has to do a couple of tricks to prevent infinite recursion when falling back:
public class JsonDerivedTypeConverer<T> : JsonConverter
{
public JsonDerivedTypeConverer() { }
public JsonDerivedTypeConverer(params Type[] types)
{
this.DerivedTypes = types;
}
readonly HashSet<Type> derivedTypes = new HashSet<Type>();
public IEnumerable<Type> DerivedTypes
{
get
{
return derivedTypes.ToArray();
}
set
{
derivedTypes.Clear();
if (value != null)
derivedTypes.UnionWith(value);
}
}
JsonObjectContract FindContract(JObject obj, JsonSerializer serializer)
{
List<JsonObjectContract> bestContracts = new List<JsonObjectContract>();
foreach (var type in derivedTypes)
{
if (type.IsAbstract)
continue;
var contract = serializer.ContractResolver.ResolveContract(type) as JsonObjectContract;
if (contract == null)
continue;
if (obj.Properties().Select(p => p.Name).Where(n => n != "$type").Any(n => contract.Properties.GetClosestMatchProperty(n) == null))
continue;
if (bestContracts.Count == 0 || bestContracts[0].Properties.Count > contract.Properties.Count)
{
bestContracts.Clear();
bestContracts.Add(contract);
}
else if (contract.Properties.Count == bestContracts[0].Properties.Count)
{
bestContracts.Add(contract);
}
}
return bestContracts.Single();
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(T);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var obj = JObject.Load(reader); // Throws an exception if the current token is not an object.
if (obj["$type"] != null && serializer.TypeNameHandling != TypeNameHandling.None)
{
// Prevent infinite recursion when using an explicit converter in the list.
var removed = serializer.Converters.Remove(this);
try
{
// Kludge to prevent infinite recursion when using JsonConverterAttribute on the type: deserialize to object.
return obj.ToObject(typeof(object), serializer);
}
finally
{
if (removed)
serializer.Converters.Add(this);
}
}
else
{
var contract = FindContract(obj, serializer);
if (contract == null)
throw new JsonSerializationException("no contract found for " + obj.ToString());
if (existingValue == null || !contract.UnderlyingType.IsAssignableFrom(existingValue.GetType()))
existingValue = contract.DefaultCreator();
using (var sr = obj.CreateReader())
{
serializer.Populate(sr, existingValue);
}
return existingValue;
}
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}

Categories