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

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.

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.

Cannot Deserialize the Current JSON Object (Empty Array)

I am trying to make this program that formats all these objects into a treeview, to do this (I'm using JSON for ordering the objects), I needed to parse the JSON, so I chose JSON.NET.
So here is an example of how the formatting is:
{
"Space": {
"ClassName": "SpaceObject",
"Name": "Space",
"Children": {
"Object1": {
"ClassName": "Object",
"Name": "Object1",
"Children": []
},
"Object2": {
"ClassName": "Object",
"Name": "Object2",
"Children": []
}
}
}
}
public class CObject
{
[JsonProperty(PropertyName = "Name")]
public string Name { get; set; }
[JsonProperty(PropertyName = "ClassName")]
public string ClassName { get; set; }
[JsonProperty(PropertyName = "Children")]
public IDictionary<string, CObject> Children { get; set; }
}
IDictionary<string, CObject> obj = JsonConvert.DeserializeObject<IDictionary<string, CObject>>(Json, new JsonSerializerSettings() {
MissingMemberHandling = MissingMemberHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore,
});
foreach (var i in obj) {
ExplorerView1.Nodes.Add(AddObject(i.Value));
}
I believe I found the error, it's due to a children array having no objects in it. I don't know how to fix this though, can anyone help?
JsonSingleOrEmptyArrayConverter<T> from this answer to Deserialize JSON when type can be different almost does what you need. It simply needs to be enhanced to allow the current contract type to be a dictionary contract as well as an object contract.
First, modify JsonSingleOrEmptyArrayConverter<T> as follows:
public class JsonSingleOrEmptyArrayConverter<T> : JsonConverter where T : class
{
//https://stackoverflow.com/questions/29449641/deserialize-json-when-type-can-be-different?rq=1
public override bool CanConvert(Type objectType)
{
return typeof(T).IsAssignableFrom(objectType);
}
public override bool CanWrite { get { return false; } }
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var contract = serializer.ContractResolver.ResolveContract(objectType);
// Allow for dictionary contracts as well as objects contracts, since both are represented by
// an unordered set of name/value pairs that begins with { (left brace) and ends with } (right brace).
if (!(contract is Newtonsoft.Json.Serialization.JsonObjectContract
|| contract is Newtonsoft.Json.Serialization.JsonDictionaryContract))
{
throw new JsonSerializationException(string.Format("Unsupported objectType {0} at {1}.", objectType, reader.Path));
}
switch (reader.SkipComments().TokenType)
{
case JsonToken.StartArray:
{
int count = 0;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.Comment:
break;
case JsonToken.EndArray:
// You might want to allocate an empty object here if existingValue is null
// If so, do
// return existingValue ?? contract.DefaultCreator();
return existingValue;
default:
{
count++;
if (count > 1)
throw new JsonSerializationException(string.Format("Too many objects at path {0}.", reader.Path));
existingValue = existingValue ?? contract.DefaultCreator();
serializer.Populate(reader, existingValue);
}
break;
}
}
// Should not come here.
throw new JsonSerializationException(string.Format("Unclosed array at path {0}.", reader.Path));
}
case JsonToken.Null:
return null;
case JsonToken.StartObject:
existingValue = existingValue ?? contract.DefaultCreator();
serializer.Populate(reader, existingValue);
return existingValue;
default:
throw new InvalidOperationException("Unexpected token type " + reader.TokenType.ToString());
}
}
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;
}
}
Then deserialize as follows:
var settings = new JsonSerializerSettings
{
MissingMemberHandling = MissingMemberHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore,
Converters = { new JsonSingleOrEmptyArrayConverter<IDictionary<string, CObject>>() },
};
var obj = JsonConvert.DeserializeObject<IDictionary<string, CObject>>(Json, settings);
Notes:
My assumption is that an empty array [] is only used when there are no children. If the array is ever nonempty, this assumption will be wrong and the converter will not work correctly.
The converter returns a null value for an empty array. If you would instead prefer an empty dictionary, uncomment the following lines:
// You might want to allocate an empty object here if existingValue is null
// If so, do
// return existingValue ?? contract.DefaultCreator();
Working .Net fiddle here.

JsonSerializer.CreateDefault().Populate(..) resets my values

I have following class:
public class MainClass
{
public static MainClass[] array = new MainClass[1]
{
new MainClass
{
subClass = new SubClass[2]
{
new SubClass
{
variable1 = "my value"
},
new SubClass
{
variable1 = "my value"
}
}
}
};
public SubClass[] subClass;
[DataContract]
public class SubClass
{
public string variable1 = "default value";
[DataMember] // because only variable2 should be saved in json
public string variable2 = "default value";
}
}
which I save as follows:
File.WriteAllText("data.txt", JsonConvert.SerializeObject(new
{
MainClass.array
}, new JsonSerializerSettings { Formatting = Formatting.Indented }));
data.txt:
{
"array": [
{
"subClass": [
{
"variable2": "value from json"
},
{
"variable2": "value from json"
}
]
}
]
}
then I deserialize and populate my object like this:
JObject json = JObject.Parse(File.ReadAllText("data.txt"));
if (json["array"] != null)
{
for (int i = 0, len = json["array"].Count(); i < len; i++)
{
using (var sr = json["array"][i].CreateReader())
{
JsonSerializer.CreateDefault().Populate(sr, MainClass.array[i]);
}
}
}
however, when I print following variables:
Console.WriteLine(MainClass.array[0].subClass[0].variable1);
Console.WriteLine(MainClass.array[0].subClass[0].variable2);
Console.WriteLine(MainClass.array[0].subClass[1].variable1);
Console.WriteLine(MainClass.array[0].subClass[1].variable2);
then output of it is:
default value
value from json
default value
value from json
but instead of "default value" there should be "my value" because that is what I used while creating an instance of class and JsonSerializer should only populate the object with values from json.
How do I properly populate the whole object without resetting its properties which are not included in json?
It looks as though JsonSerializer.Populate() lacks the MergeArrayHandling setting that is available for JObject.Merge(). Through testing I have found that:
Populating members that are arrays or some other type of read-only collection seems to work like MergeArrayHandling.Replace.
This is the behavior you are experiencing -- the existing array and all the items therein are being discarded and replaced with a fresh array containing newly constructed items that have default values. In contrast, you require MergeArrayHandling.Merge: Merge array items together, matched by index.
Populating members that are read/write collections such as List<T> seems to work like MergeArrayHandling.Concat.
It seems reasonable to request an enhancement that Populate() support this setting -- though I don't know how easy it would be to implement. At the minimum the documentation for Populate() should explain this behavior.
In the meantime, here's a custom JsonConverter with the necessary logic to emulate the behavior of MergeArrayHandling.Merge:
public class ArrayMergeConverter<T> : ArrayMergeConverter
{
public override bool CanConvert(Type objectType)
{
return objectType.IsArray && objectType.GetArrayRank() == 1 && objectType.GetElementType() == typeof(T);
}
}
public class ArrayMergeConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (!objectType.IsArray)
throw new JsonSerializationException(string.Format("Non-array type {0} not supported.", objectType));
var contract = (JsonArrayContract)serializer.ContractResolver.ResolveContract(objectType);
if (contract.IsMultidimensionalArray)
throw new JsonSerializationException("Multidimensional arrays not supported.");
if (reader.TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.StartArray)
throw new JsonSerializationException(string.Format("Invalid start token: {0}", reader.TokenType));
var itemType = contract.CollectionItemType;
var existingList = existingValue as IList;
IList list = new List<object>();
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.Comment:
break;
case JsonToken.Null:
list.Add(null);
break;
case JsonToken.EndArray:
var array = Array.CreateInstance(itemType, list.Count);
list.CopyTo(array, 0);
return array;
default:
// Add item to list
var existingItem = existingList != null && list.Count < existingList.Count ? existingList[list.Count] : null;
if (existingItem == null)
{
existingItem = serializer.Deserialize(reader, itemType);
}
else
{
serializer.Populate(reader, existingItem);
}
list.Add(existingItem);
break;
}
}
// Should not come here.
throw new JsonSerializationException("Unclosed array 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 override bool CanConvert(Type objectType)
{
throw new NotImplementedException();
}
}
Then add the converter to your subClass member as follows:
[JsonConverter(typeof(ArrayMergeConverter))]
public SubClass[] subClass;
Or, if you don't want to add Json.NET attributes to your data model, you can add it in serializer settings:
var settings = new JsonSerializerSettings
{
Converters = new[] { new ArrayMergeConverter<MainClass.SubClass>() },
};
JsonSerializer.CreateDefault(settings).Populate(sr, MainClass.array[i]);
The converter is specifically designed for arrays but a similar converter could easily be created for read/write collections such as List<T>.

Json.NET Deserialization into dynamic object with referencing

How can I get Json.NET to deserialize into dynamic objects but still do reference resolution?
dynamic d=JsonConvert.DeserializeObject<ExpandoObject>(...) just as
dynamic d=JsonConvert.DeserializeObject(...) returns a dynamic object but they don't resolve the $ref and $id parts. (An ExpandoObject eo for example will only have eo["$ref"]="..." and doesn't have the properties it should have because it's not the same as the $id-Object)
What I've found out is that I need the contract resolver resolve to a dynamic contract - which ExpandoObject only does if I explicitly tell Json.NET with a custom ContractResolver.
Still It seems the ExpandoObject is parsed with it's own Converter and it fails again.
I've tried a custom class inheriting from IDynamicMetaObjectProvider which resulted in an infinite loop and didn't seem like the right thing. I would actually expect some easy solution to get ExpandoObject to have reference resolution.
Any help?
Since Json.NET is open source and its MIT license allows modification, the easiest solution may be to adapt its ExpandoObjectConverter to your needs:
/// <summary>
/// Converts an ExpandoObject to and from JSON, handling object references.
/// </summary>
public class ObjectReferenceExpandoObjectConverter : JsonConverter
{
// Adapted from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Converters/ExpandoObjectConverter.cs
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// can write is set to false
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return ReadValue(serializer, reader);
}
private object ReadValue(JsonSerializer serializer, JsonReader reader)
{
while (reader.TokenType == JsonToken.Comment)
{
if (!reader.Read())
throw reader.CreateException("Unexpected end when reading ExpandoObject.");
}
switch (reader.TokenType)
{
case JsonToken.StartObject:
return ReadObject(serializer, reader);
case JsonToken.StartArray:
return ReadList(serializer, reader);
default:
if (JsonTokenUtils.IsPrimitiveToken(reader.TokenType))
return reader.Value;
throw reader.CreateException("Unexpected token when converting ExpandoObject");
}
}
private object ReadList(JsonSerializer serializer, JsonReader reader)
{
IList<object> list = new List<object>();
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.Comment:
break;
default:
object v = ReadValue(serializer, reader);
list.Add(v);
break;
case JsonToken.EndArray:
return list;
}
}
throw reader.CreateException("Unexpected end when reading ExpandoObject.");
}
private object ReadObject(JsonSerializer serializer, JsonReader reader)
{
IDictionary<string, object> expandoObject = null;
object referenceObject = null;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.PropertyName:
string propertyName = reader.Value.ToString();
if (!reader.Read())
throw new InvalidOperationException("Unexpected end when reading ExpandoObject.");
object v = ReadValue(serializer, reader);
if (propertyName == "$ref")
{
var id = (v == null ? null : Convert.ToString(v, CultureInfo.InvariantCulture));
referenceObject = serializer.ReferenceResolver.ResolveReference(serializer, id);
}
else if (propertyName == "$id")
{
var id = (v == null ? null : Convert.ToString(v, CultureInfo.InvariantCulture));
serializer.ReferenceResolver.AddReference(serializer, id, (expandoObject ?? (expandoObject = new ExpandoObject())));
}
else
{
(expandoObject ?? (expandoObject = new ExpandoObject()))[propertyName] = v;
}
break;
case JsonToken.Comment:
break;
case JsonToken.EndObject:
if (referenceObject != null && expandoObject != null)
throw reader.CreateException("ExpandoObject contained both $ref and real data");
return referenceObject ?? expandoObject;
}
}
throw reader.CreateException("Unexpected end when reading ExpandoObject.");
}
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(ExpandoObject));
}
public override bool CanWrite
{
get { return false; }
}
}
public static class JsonTokenUtils
{
// Adapted from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Utilities/JsonTokenUtils.cs
public static bool IsPrimitiveToken(this JsonToken token)
{
switch (token)
{
case JsonToken.Integer:
case JsonToken.Float:
case JsonToken.String:
case JsonToken.Boolean:
case JsonToken.Undefined:
case JsonToken.Null:
case JsonToken.Date:
case JsonToken.Bytes:
return true;
default:
return false;
}
}
}
public static class JsonReaderExtensions
{
public static JsonSerializationException CreateException(this JsonReader reader, string format, params object[] args)
{
// Adapted from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/JsonPosition.cs
var lineInfo = reader as IJsonLineInfo;
var path = (reader == null ? null : reader.Path);
var message = string.Format(CultureInfo.InvariantCulture, format, args);
if (!message.EndsWith(Environment.NewLine, StringComparison.Ordinal))
{
message = message.Trim();
if (!message.EndsWith(".", StringComparison.Ordinal))
message += ".";
message += " ";
}
message += string.Format(CultureInfo.InvariantCulture, "Path '{0}'", path);
if (lineInfo != null && lineInfo.HasLineInfo())
message += string.Format(CultureInfo.InvariantCulture, ", line {0}, position {1}", lineInfo.LineNumber, lineInfo.LinePosition);
message += ".";
return new JsonSerializationException(message);
}
}
And then use it like:
var settings = new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects, ReferenceLoopHandling = ReferenceLoopHandling.Serialize };
settings.Converters.Add(new ObjectReferenceExpandoObjectConverter());
dynamic d = JsonConvert.DeserializeObject<ExpandoObject>(json, settings);
The way I did it now is with a postprocessing step and a recursive function that's doing its own reference saving and rewiring:
private static void Reffing(this IDictionary<string, object> current, Action<object> exchange,IDictionary<string, object> refdic)
{
object value;
if(current.TryGetValue("$ref", out value))
{
if(!refdic.TryGetValue((string) value, out value))
throw new Exception("ref not found ");
if (exchange != null)
exchange(value);
return;
}
if (current.TryGetValue("$id", out value))
{
refdic[(string) value] = current;
}
foreach (var kvp in current.ToList())
{
if (kvp.Key.StartsWith("$"))
continue;
var expandoObject = kvp.Value as ExpandoObject;
if(expandoObject != null)
Reffing(expandoObject,o => current[kvp.Key]=o,refdic);
var list = kvp.Value as IList<object>;
if (list == null) continue;
for (var i = 0; i < list.Count; i++)
{
var lEO = list[i] as ExpandoObject;
if(lEO!=null)
Reffing(lEO,o => list[i]=o,refdic);
}
}
}
used as:
var test = JsonConvert.DeserializeObject<ExpandoObject>(...);
var dictionary = new Dictionary<string, object>();
Reffing(test,null,dictionary);

Categories