Json.NET - ContractResolver to serialize dictionary with non-primitive keys - c#

I have some .NET classes with localized text; i.e. text in English, corresponding text in Spanish, etc. We have a Locale class that looks something like this:
class Locale
{
int Id;
string Abbreviation; // e.g. "en"
string Name; // e.g. "English"
static Locale FromAbbreviation(string abbreviation);
}
Localized text is stored in IDictionary properties, something like
class Document
{
IDictionary<Locale, string> Content;
}
When serializing to JSON, I would like this to be keyed by locale abbreviation, so a serialized Document object would look something like this:
{
"content": {
"en": "English content",
"es": "Spanish content"
}
}
I need a ContractResolver that converts an IDictionary<Locale, string> object to a Dictionary<string, string> object, using the Locale.Abbreviation property as the key during serialization, and calling Locale.FromAbbreviation() on deserialization to convert the key back to a Locale object.
I have looked at the JSON.NET documentation and various Stackoverflow questions, and there does not seem to be an easy way to do this (at least I can't find it). I did find what looks like a straightforward way to do the same thing using a TypeConverter attribute, but I would rather not take a dependence on Json.NET from my domain classes. Is there a reasonable way to do this using a ContractResolver?

You don't really need a ContractResolver here. You can use a custom JsonConverter to handle the conversion from Dictionary<Locale, string> to your desired JSON and back without needing to use an attribute in your model classes. As long as the converter's CanConvert method is coded correctly to identify the dictionary type that the converter handles, then you can just add the converter to the serializer settings and Json.Net will find it and use it appropriately.
Here is what the converter might look like:
class LocaleDictionaryConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(IDictionary<Locale, string>).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JObject obj = JObject.Load(reader);
IDictionary<Locale, string> dict = (IDictionary<Locale, string>)existingValue ?? new Dictionary<Locale, string>();
foreach (var prop in obj.Properties())
{
dict.Add(Locale.FromAbbreviation(prop.Name), (string)prop.Value);
}
return dict;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
IDictionary<Locale, string> dict = (IDictionary<Locale, string>)value;
JObject obj = new JObject();
foreach (var kvp in dict)
{
obj.Add(kvp.Key.Abbreviation, kvp.Value);
}
obj.WriteTo(writer);
}
}
And here is how you would use it to serialize:
var settings = new JsonSerializerSettings();
settings.Converters.Add(new LocaleDictionaryConverter());
settings.Formatting = Formatting.Indented;
string json = JsonConvert.SerializeObject(document, settings);
And deserialize:
// same settings as above
var document = JsonConvert.DeserializeObject<Document>(json, settings);
Here is a demo: https://dotnetfiddle.net/f1rXl2

Related

Using a TypeConverter for a Dictionary key, but not Dictionary values of the same type?

I'm trying to use Newtonsoft.Json to serialize and deserialize a Dictionary<(int, int), MyClass>. Because (int, int) has to get serialized to a string, I have to provide a custom TypeConverter to deserialize it back to a tuple:
public class Tuple2Converter<T1, T2> : TypeConverter {
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) {
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) {
var parts = Convert.ToString(value).Trim('(').Trim(')').Split(", ");
var item1 = (T1)TypeDescriptor.GetConverter(typeof(T1)).ConvertFromInvariantString(parts[0])!;
var item2 = (T2)TypeDescriptor.GetConverter(typeof(T2)).ConvertFromInvariantString(parts[1])!;
return (item1, item2);
}
}
// ...
TypeDescriptor.AddAttributes(typeof((int, int)), new TypeConverterAttribute(typeof(Tuple2Converter<int, int>)));
var resultingObject =
JsonConvert.DeserializeObject<Dictionary<(int Q, int R), HexWithMeta>>(dictString);
However, when deserializing I now get the error:
Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.ValueTuple`2[System.Int32,System.Int32]' because the type requires a JSON string value to deserialize correctly.
To fix this error either change the JSON to a JSON string value or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object.
It's trying to use the custom TypeConverter to convert another (int, int) to a C# tuple, but this time, the tuple was serialized in a standard way (JSON object instead of string), because this tuple is one that exists on MyClass, so it was serialized like:
"QR": {
"Item1": 0,
"Item2": 0
}
How can I get Newtonsoft.Json to use the custom TypeConverter when deserializing the string-encoded tuple on the Dictionary key, but not for any tuples contained within the Dictionary's serialized values?
Note that I am only globally binding my TypeConverter via TypeDescriptor.AddAttributes() to get correct JSON serialization, I don't need to do it for other reasons.
Even using a custom contract resolver, Json.NET doesn't have a convenient way to inject a custom method to convert dictionary keys from and to JSON property names.[1] Binding in a global TypeConverter is the documented way to bind JSON property names to a complex dictionary key type. But, as you have found, binding in a custom TypeConverter for a complex type will also cause that type to be serialized as a string when serialized standalone as well as when formatted as a dictionary key.
Thus if you want to use a custom string conversion logic for a specific dictionary key type (here ValueTuple<int, int>) but not use the same logic when serializing that type standalone, you have the following options:
You could bind a TypeConverter globally as you are currently doing, then cancel use of the TypeConverter via a custom JsonConverter or contract resolver as shown in this answer to Newtonsoft.JSON cannot convert model with TypeConverter attribute.
You could skip using the TypeConverter and create a custom JsonConverter for your Dictionary<(int, int), MyClass>.
Since binding a TypeConverter globally via TypeDescriptor.AddAttributes might have unexpected side-effects, and you don't particularly want to do it anyway, I recommend the custom JsonConverter approach. Here is one that works:
public class DictionaryTupleConverter<T1, T2, TValue> : JsonConverter<Dictionary<(T1, T2), TValue>>
{
readonly TypeConverter converter1 = TypeDescriptor.GetConverter(typeof(T1)); // Cache for performance
readonly TypeConverter converter2 = TypeDescriptor.GetConverter(typeof(T2));
public override Dictionary<(T1, T2), TValue>? ReadJson(JsonReader reader, Type objectType, Dictionary<(T1, T2), TValue>? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
var innerDictionary = serializer.Deserialize<Dictionary<string, TValue>>(reader);
if (innerDictionary == null)
return null;
var dictionary = existingValue ?? (Dictionary<(T1, T2), TValue>)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator!();
foreach (var pair in innerDictionary)
dictionary.Add(ConvertFrom(pair.Key), pair.Value);
return dictionary;
}
public override void WriteJson(JsonWriter writer, Dictionary<(T1, T2), TValue>? value, JsonSerializer serializer) =>
serializer.Serialize(writer, value!.ToDictionary(p => ConvertTo(p.Key)!, p => p.Value));
(T1, T2) ConvertFrom(string value)
{
var parts = value.Trim('(').Trim(')').Split(",");
var item1 = (T1)converter1.ConvertFromInvariantString(parts[0].Trim())!;
var item2 = (T2)converter2.ConvertFromInvariantString(parts[1].Trim())!;
return (item1, item2);
}
string ConvertTo((T1, T2) value)
{
var s1 = converter1.ConvertToInvariantString(value.Item1)!;
var s2 = converter2.ConvertToInvariantString(value.Item2)!;
return string.Format("({0},{1})", s1, s2);
}
}
Then serialize and deserialize using the following settings:
var settings = new JsonSerializerSettings
{
Converters = { new DictionaryTupleConverter<int, int, HexWithMeta>() },
};
Notes:
I am unsure whether ValueTuple<T1, T2>.ToString() is locale-invariant (this github issue suggests not), so I recommend creating your own method to convert your tuples to strings that is verifiably locale-invariant.
Your conversion methods for (T1, T2) from and to a string do not take into account the possibility that the inner strings may contain commas or parentheses. You may want to enhance the parsing and formatting logic with some sort of simple CSV parser.
Demo fiddle here.
[1] For confirmation you may check the source code for JsonSerializerInternalWriter.GetPropertyName(), the method used to generate a JSON property name from a dictionary key. The method is not virtual and is implemented as a series of hardcoded checks with no option to inject custom logic.

Json.NET TypeNameHandling for reading only

I am using the following JsonSerializerSettings:
new JsonSerializerSettings {
TypeNameHandling = TypeNameHandling.Objects
}
Gets or sets how type name writing and reading is handled by the serializer.
I was wondering if there is any way to enable type name reading, but disable it during writing.
I am deserializing a list of abstract base classes, so I need the $type nodes on my JSON to be able to deserialize (using a custom SerializationBinder), but I don't want my responses to include their types and namespaces.
Note that I am referring to the (de)serialization in the context of ASP.NET Web API where I can only set my SerializerSettings on a global level on my JsonFormatter in Configuration.Formatters or on Controller level using IControllerConfiguration. These settings are always used for both reading and writing.
As this TypeNameHandling was only needed for a specific DTO, I just created a custom JsonConverter:
public class CustomJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(MyDTO);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var customSerializer = new JsonSerializer {
TypeNameHandling = TypeNameHandling.Objects,
Binder = new CustomSerializationBinder()
};
return customSerializer.Deserialize(reader, objectType);
}
}
And registered it on my JsonFormatter:
Configuration.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new CustomJsonConverter());
Whether or not TypeNameHandling is used may be defined on per SerializeObject/DeserializeObject basis. So just don't use these settings when you don't need that $type:
var typedJson = #"{""$type"":""ConsoleApp2.Program+TestData, ConsoleApp2"",""TestField"":0}";
var testData = JsonConvert.DeserializeObject(typedJson, new JsonSerializerSettings {
TypeNameHandling = TypeNameHandling.Objects
});
var json = JsonConvert.SerializeObject(testData); // <----- Notice no settings here
Console.WriteLine(json);
// Outputs:
// {"TestField":0}

Ignore duplicates when serializing array with JSON.Net

Clarification (to anyone in the same situation):
Note that my task is to serialize an existing legacy object. As such, I would prefer to tune the serializer rather than interfere with the data structure.
I believe in most cases it's better to remove the duplicates directly from the data, as indicated by #danny-chen's answer.
As part of my object that I want to serialize with JSON.Net, there is a string[] files property which contains duplicates:
some/path/to/f1.jpg
some/path/to/f1.jpg
some/path/to/f2.jpg
some/path/to/f3.jpg
some/path/to/f2.jpg
And let's suppose these are not necessarily in order (f2, f3, f2).
Is it possible to serialize the array and ignore the duplicates ? Expected result:
{
"files": [
"some/path/to/f1.jpg",
"some/path/to/f2.jpg",
"some/path/to/f3.jpg"
]
}
I have tried the PreserveReferencesHandling setting, but it didn't work as each file in the array is a different object, with a possibly repeated value.
It's not part of the serialization, it's part of the data processing. I suggest you remove the duplicates before serialization.
string[] files = GetFiles();
data.Files = files.Distinct().ToArray();
//serialize data
//instead of data.Files = files; and do tricky things in serialization
The simplest solution is to filter the list before serialization as suggested by #Danny Chen. However, if you absolutely have to do it during serialization you can use a custom JsonConverter.
Here is the code you would need:
public class RemoveDuplicatesConverter<T> : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(IEnumerable<T>).IsAssignableFrom(objectType);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteStartArray();
foreach (T item in ((IEnumerable<T>)value).Distinct())
{
serializer.Serialize(writer, item);
}
writer.WriteEndArray();
}
public override bool CanRead
{
get { return false; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
To use the converter, add a [JsonConverter] attribute to the list or array property in your class for which you'd like to remove duplicates, as shown below. Be sure the generic type of the converter matches the type of your list.
class Foo
{
[JsonProperty("files")]
[JsonConverter(typeof(RemoveDuplicatesConverter<string>))]
public string[] Files { get; set; }
}
Then serialize as normal. The list in the JSON will have the duplicates removed, but the original list in your object will be unaffected.
string json = JsonConvert.SerializeObject(your_object, Formatting.Indented);
Fiddle: https://dotnetfiddle.net/vs2oWQ

Json.NET does not preserve primitive type information in lists or dictionaries of objects. Is there a workaround?

The following example illustrates a fundamental flaw in Json.NET's type handling:
List<object> items = new List<object>() {Guid.NewGuid(),DateTime.Now};
var settings = new JsonSerializerSettings() { TypeNameHandling=TypeNameHandling.All };
var json = JsonConvert.SerializeObject<List<object>>(value,settings);
resulting in the following JSON:
{"$type":"System.Collections.Generic.List`1[[System.Object, mscorlib]], mscorlib","$values":["9d7aa4d3-a340-4cee-baa8-6af0582b8acd","2014-07-28T21:03:17.1287029-04:00"]}
As you can see the list items have lost their type information. Deserializing that same JSON will result in a list containing just strings.
This issue was previously reported on codeplex and perfunctorily closed, stating including the type information would make the JSON too messy. I am surprised we aren't given a separate option to include primitive type information for such scenarios as the round-trip consistency is broken.
https://json.codeplex.com/workitem/23833
I would expect the data to come back with the same type information that it left with.
Does anybody have any suggestions or workarounds to remedy this undesired behavior?
Thanks,
Chris
Here is a solution using a custom JsonConverter:
public sealed class PrimitiveJsonConverter : JsonConverter
{
public PrimitiveJsonConverter()
{
}
public override bool CanRead
{
get
{
return false;
}
}
public override bool CanConvert(Type objectType)
{
return objectType.IsPrimitive;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
switch (serializer.TypeNameHandling)
{
case TypeNameHandling.All:
writer.WriteStartObject();
writer.WritePropertyName("$type", false);
switch (serializer.TypeNameAssemblyFormat)
{
case FormatterAssemblyStyle.Full:
writer.WriteValue(value.GetType().AssemblyQualifiedName);
break;
default:
writer.WriteValue(value.GetType().FullName);
break;
}
writer.WritePropertyName("$value", false);
writer.WriteValue(value);
writer.WriteEndObject();
break;
default:
writer.WriteValue(value);
break;
}
}
}
Here is how to use it:
JsonSerializerSettings settings = new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.All,
};
settings.Converters.Insert(0, new PrimitiveJsonConverter());
return JsonConvert.SerializeObject(myDotNetObject, settings);
I'm currently using this solution to serialize an IDictionary<string, object> instance that can contain primitives.
Hacked this together and tested it out. Obviously this needs unit testing and is more a proof of concept. If you want a dirty solution to get you going this should get one started.
https://github.com/xstos/Newtonsoft.Json/commit/8d3507cbba78f7096a82e42973e56d69c9541c42

Serializing strings containing apostrophes with JSON.Net

I am using JSON.Net as my serializer for a large MVC 3 web application in c# and the Razor view engine. For the initial page load in one view, there is a large amount of JSON dumped inside a script tag using #Html.Raw(JsonConvert.SerializeObject(myObject)).
The problem is that some values of some objects contain apostrophes (think names like O'Brien), which JSON.Net is not escaping or encoding in any way.
It's not an option to pre-encode the values stored in the database because that vastly complicates various other processes.
Is there a way to force JSON.Net to HTML encode the values of the objects that it serializes, the same way that the built-in JavaScriptSerializer does when you call JavaScriptSerializer.Serialize(myObject)? Or, is there a way to deal with this in the view?
JsonSerializerSettings settings = new JsonSerializerSettings
{
StringEscapeHandling = StringEscapeHandling.EscapeHtml
};
JsonConvert.SerializeObject(obj, settings);
Though there are some cases wherein you might want to drop some JSON into your page as a JavaScript string, or an HTML attribute value, most often what you'd do is simply include it directly into JavaScript source, because JSON is valid JavaScript syntax after all.
You can create custom JsonConverter like this:
public class EscapeQuoteConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString().Replace("'", "\\'"));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var value = JToken.Load(reader).Value<string>();
return value.Replace("\\'", "'");
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(string);
}
}
To use this only for Name property specify it by attribute:
public class Person
{
[JsonConverter(typeof(EscapeQuoteConverter))]
public string Name { get; set; }
}
To apply Converter to all strings use:
JsonConvert.SerializeObject(person, Formatting.Indented, new EscapeQuoteConverter());
Use System.Web.HttpUtility.HtmlEncode
HttpUtility.HtmlEncode(JsonConvert.SerializeObject(myObject))

Categories

Resources