Creating a custom converter to write json as a Dictionarystring, IObject from an object of String Key IObject Value
One of the easiest ways to write a JsonConverter
is to map the object to be serialized to some DTO, then (de)serialize the DTO. Since your PDItem
looks like a single dictionary key/value pair and is serialized like a dictionary, the easiest DTO to use would be an actual dictionary, namely Dictionary<string, List<IItem>>
.
Thus your PDItemConverter
can be written as follows:
public class PDItemConverter: JsonConverter<PDItem>
{
public override void WriteJson(JsonWriter writer, PDItem value, JsonSerializer serializer)
{
// Convert to a dictionary DTO and serialize
serializer.Serialize(writer, new Dictionary<string, List<IItem>> { { value.Key, value.Value } });
}
public override PDItem ReadJson(JsonReader reader, Type objectType, PDItem existingValue,
bool hasExistingValue, JsonSerializer serializer)
{
// Deserialize as a dictionary DTO and map to a PDItem
var dto = serializer.Deserialize<Dictionary<string, List<IItem>>>(reader);
if (dto == null)
return null;
if (dto.Count != 1)
throw new JsonSerializationException(string.Format("Incorrect number of dictionary keys: {0}", dto.Count));
var pair = dto.First();
existingValue = hasExistingValue ? existingValue : new PDItem();
existingValue.Key = pair.Key;
existingValue.Value = pair.Value;
return existingValue;
}
}
Since you are (de)serializing using the incoming serializer
any converters associated to nested types such as IItem
will get picked up and used automatically.
In addition, in JsonCreationConverter<T>
you need to override CanWrite
and return false. This causes the serializer to fall back to default serialization when writing JSON as explained in this answer to How to use default serialization in a custom JsonConverter. Also, I don't recommend catching and swallowing JsonReaderException
. This exception is thrown when the JSON file itself is malformed, e.g. by being truncated. Ignoring this exception and continuing can occasionally force Newtonsoft to fall into an infinite loop. Instead, propagate the exception up to the application:
public abstract class JsonCreationConverter<T> : JsonConverter
{
// Override CanWrite and return false
public override bool CanWrite { get { return false; } }
protected abstract T Create(Type objectType, JObject jObject);
public override bool CanConvert(Type objectType)
{
return typeof(T) == objectType;
}
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
var jObject = JObject.Load(reader);
var target = Create(objectType, jObject);
serializer.Populate(jObject.CreateReader(), target);
return target;
}
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Demo fiddle here.
JsonConvert dictionary with key of type Vector2Int
Your custom serializer that derives from JsonConverter
needs more work and there are few errors. First, note that you are trying to serialize/de-serialize Dictionary
of Vector2Int
not just a Vector2Int
variable.
It's this:
public Dictionary<Vector2Int, Collection> cardLocations.
not
public Vector2Int cardLocations;
Because of the above statement, your override bool CanConvert(Type objectType)
function should be checking for typeof(Dictionary<Vector2Int, Collection>)
not typeof(Vector2Int)
. Also, you also need to add checks to determine when to run the custom de-serializer.
Serializing:
1.In the WriteJson
function that serializes the json, you only need to customize the Dictionary<Vector2Int, Collection>
part which is throwing the exception so make sure that the custom code runs only when the type is Dictionary<Vector2Int, Collection>
. You can do this by putting the code inside if (value is Dictionary<Vector2Int, Collection>)
.
2.Obtain the Dictionary to serialize from the second argument. Call writer.WriteStartArray()
so that you'll write every value as an array. Now, loop over the Dictionary
, write the key with writer.WriteValue(key)
then write the value with serializer.Serialize(writer, entry.Value);
.
3. After/outside the loop call writer.WriteStartArray();
then return from that function.
If the if (value is Dictionary<Vector2Int, Collection>)
from #1 is false
then simply call writer.WriteStartObject()
followed by writer.WriteEndObject()
.
De-Serializing:
4.Determine when to run the custom de-serializer
We used if (value is Dictionary<Vector2Int, Collection>)
in #1 when serializing to determine if the custom serializer code should run but for de-serializing, we use if (reader.TokenType == JsonToken.StartArray)
to determine if we have reached the array part where we did our custom serialization in the ReadJson
function.
5.Use JArray.Load(reader);
to obtain the array data we serialized. In the returned array, the first element in it is the key in the Dictionary
.The second element is the value in the Dictionary
. The third element is the second key in the Dictionary
. The fourth element is the second value in the Dictionary
ans so on.
6.Separate the keys and values in the JArray
.
To simplify this, loop over the JArray
increment by 2
instead of 1
. By incrementing by 2
, the key can be easily be retrieved with JArray[loop + 0]
and the value can be retrieved in the-same loop with JArray[loop + 1]
.
for (int i = 0; i < jArray.Count; i += 2)
{
//Key
string key = jArray[i + 0].ToString();
//Value
string value = jArray[i + 1].ToString();
}
7.Get the key in Vector2Int
format.
Simply de-serialize the key from #6 into Vector2Int
with JsonConvert.DeserializeObject<Vector2Int>(key)
.
8.Get the value in Collection
format.
Simply de-serialize the value from #6 into Collection
with JsonConvert.DeserializeObject<Collection>(value)
.
9.Reconstruct the data
Create new instance of Dictionary<Vector2Int, Collection>
then add both the key from #7 and value from #8 to it and then return it from the ReadJson
function.
If the if (reader.TokenType == JsonToken.StartArray)
from #4 is false
, simply create and return new instance of Dictionary<Vector2Int, Collection>
from the ReadJson
function without adding key or value to it.
class Vec2DictionaryConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(Dictionary<Vector2Int, Collection>).IsAssignableFrom(objectType);
}
//Deserialize json to an Object
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
//Debug.Log("De-serializing!");
if (reader.TokenType == JsonToken.StartArray)
{
// Load JArray from stream
JArray jArray = JArray.Load(reader);
//Where to re-create the json data into
Dictionary<Vector2Int, Collection> dict = new Dictionary<Vector2Int, Collection>();
if (jArray == null || jArray.Count < 2)
{
return dict;
}
//Do the loop faster with +=2
for (int i = 0; i < jArray.Count; i += 2)
{
//first item = key
string firstData = jArray[i + 0].ToString();
//second item = value
string secondData = jArray[i + 1].ToString();
//Create Vector2Int key data
Vector2Int vect = JsonConvert.DeserializeObject<Vector2Int>(firstData);
//Create Collection value data
Collection values = JsonConvert.DeserializeObject<Collection>(secondData);
//Add both Key and Value to the Dictionary if key doesnt exit yet
if (!dict.ContainsKey(vect))
dict.Add(vect, values);
}
//Return the Dictionary result
return dict;
}
return new Dictionary<Vector2Int, Collection>();
}
//SerializeObject to Json
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
//Debug.Log("Serializing!");
if (value is Dictionary<Vector2Int, Collection>)
{
//Get the Data to serialize
Dictionary<Vector2Int, Collection> dict = (Dictionary<Vector2Int, Collection>)value;
//Loop over the Dictionary array and write each one
writer.WriteStartArray();
foreach (KeyValuePair<Vector2Int, Collection> entry in dict)
{
//Write Key (Vector)
serializer.Serialize(writer, entry.Key);
//Write Value (Collection)
serializer.Serialize(writer, entry.Value);
}
writer.WriteEndArray();
return;
}
writer.WriteStartObject();
writer.WriteEndObject();
}
}
Usage:
Serialize
string json = JsonConvert.SerializeObject(userInfo, new Vec2DictionaryConverter());
De-serialize
UserInfo obj = JsonConvert.DeserializeObject<UserInfo>(json, new Vec2DictionaryConverter());
Json.net - How to preserve dictionary value references when populating a dictionary?
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 aList<T>
is populated the new values are appended to the list, soAssert.AreSame(to_serialize[0], model);
passes purely by luck. If you had additionally assertedAssert.AreSame(1, to_serialize.Count)
it would have failed.While the converter will work for primitive keys such as
string
andint
it may not work for key types that require JSON-specific conversion such asenum
orDateTime
.The converter is currently only implemented for
Dictionary<TKey, TValue>
and takes advantage of the fact that this type implements the non-genericIDictionary
interface. It could be extended to other dictionary types such asSortedDictionary<TKey,TValue>
if required.
Demo fiddle here.
Related Topics
Why Won't Anyone Accept Public Fields in C#
Read from a File Starting at the End, Similar to Tail
Date Difference in Years Using C#
Serilog Mssql Sink Doesn't Write Logs to Database
Httpcontext.Current.Session Is Null in Ashx File
How to Connect to Any of the Specified MySQL Hosts. C# MySQL
Simulating Keyboard with Sendinput API in Directinput Applications
Mapping Database Views to Ef 5.0 Code First W/Migrations
Create Out-Of-Process Com in C#/.Net
How to Get Current User Who's Accessing an ASP.NET Application
An Expression Tree Lambda May Not Contain a Null Propagating Operator
Binary to Corresponding Ascii String Conversion
How to Programmatically Change Printer Settings with the Webbrowser Control