Skip to content

Commit dde789b

Browse files
committed
Fix dictionary serialization
Change in b062c5e did not handle a number of dictionary cases including - types that implement IReadOnlyDictionary<TKey,TValue> - types that implement IDictionary<TKey, TValue> - non-generic types that inherit a closed generic dictionary type - types that implement IDictionary that are not generic Include serialization tests for all the above cases and additionally, serialize null when the cast value in the json converter is null (cherry picked from commit 9557b99)
1 parent 813b088 commit dde789b

File tree

5 files changed

+391
-31
lines changed

5 files changed

+391
-31
lines changed

src/Nest/CommonAbstractions/SerializationBehavior/ElasticContractResolver.cs

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ public class ElasticContractResolver : DefaultContractResolver
1818
public static JsonSerializer Empty { get; } = new JsonSerializer();
1919

2020
/// <summary>
21-
/// ConnectionSettings can be requested by JsonConverter's.
21+
/// ConnectionSettings can be requested by JsonConverters.
2222
/// </summary>
23-
public IConnectionSettingsValues ConnectionSettings { get; private set; }
23+
public IConnectionSettingsValues ConnectionSettings { get; }
2424

2525
/// <summary>
2626
/// Signals to custom converter that it can get serialization state from one of the converters. Ugly but massive performance gain
@@ -40,21 +40,16 @@ protected override JsonContract CreateContract(Type objectType)
4040
{
4141
var contract = base.CreateContract(o);
4242

43-
if (typeof(IDictionary).IsAssignableFrom(o) && !typeof(IIsADictionary).IsAssignableFrom(o))
43+
if ((typeof(IDictionary).IsAssignableFrom(o) || o.IsGenericDictionary()) && !typeof(IIsADictionary).IsAssignableFrom(o))
4444
{
45-
if (!o.IsGeneric())
46-
contract.Converter = new VerbatimDictionaryKeysJsonConverter<object, object>();
45+
Type[] genericArguments;
46+
if (!o.TryGetGenericDictionaryArguments(out genericArguments))
47+
contract.Converter = new VerbatimDictionaryKeysJsonConverter();
4748
else
48-
{
49-
var genericArguments = o.GetGenericArguments();
50-
if (genericArguments.Length != 2)
51-
contract.Converter = new VerbatimDictionaryKeysJsonConverter<object, object>();
52-
else
53-
contract.Converter =
54-
(JsonConverter)typeof(VerbatimDictionaryKeysJsonConverter<,>).CreateGenericInstance(genericArguments);
55-
}
49+
contract.Converter =
50+
(JsonConverter)typeof(VerbatimDictionaryKeysJsonConverter<,>).CreateGenericInstance(genericArguments);
5651
}
57-
if (typeof(IEnumerable<QueryContainer>).IsAssignableFrom(o))
52+
else if (typeof(IEnumerable<QueryContainer>).IsAssignableFrom(o))
5853
contract.Converter = new QueryContainerCollectionJsonConverter();
5954
else if (o == typeof(ServerError))
6055
contract.Converter = new ServerErrorJsonConverter();

src/Nest/CommonAbstractions/SerializationBehavior/GenericJsonConverters/VerbatimDictionaryKeysConverter.cs

Lines changed: 111 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,105 @@ namespace Nest
1212
/// JSON converter for IDictionary that ignores the contract resolver (e.g. CamelCaseFieldNamesContractResolver)
1313
/// when converting dictionary keys to property names.
1414
/// </summary>
15+
internal class VerbatimDictionaryKeysJsonConverter : JsonConverter
16+
{
17+
public override bool CanConvert(Type t) => typeof(IDictionary).IsAssignableFrom(t);
18+
19+
public override bool CanRead => false;
20+
21+
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
22+
{
23+
throw new NotSupportedException();
24+
}
25+
26+
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
27+
{
28+
var dictionary = (IDictionary)value;
29+
if (dictionary == null)
30+
{
31+
writer.WriteNull();
32+
return;
33+
}
34+
35+
var settings = serializer.GetConnectionSettings();
36+
var seenEntries = new Dictionary<string, object>(dictionary.Count);
37+
38+
Field fieldName;
39+
PropertyName propertyName;
40+
IndexName indexName;
41+
TypeName typeName;
42+
43+
foreach (DictionaryEntry entry in dictionary)
44+
{
45+
if (entry.Value == null && serializer.NullValueHandling == NullValueHandling.Ignore)
46+
continue;
47+
string key;
48+
if (settings == null)
49+
key = Convert.ToString(entry.Key, CultureInfo.InvariantCulture);
50+
else if (AsType(entry.Key, out fieldName))
51+
{
52+
key = settings.Inferrer.Field(fieldName);
53+
}
54+
else if (AsType(entry.Key, out propertyName))
55+
{
56+
if (propertyName?.Property != null)
57+
{
58+
IPropertyMapping mapping;
59+
if (settings.PropertyMappings.TryGetValue(propertyName.Property, out mapping) && mapping.Ignore)
60+
{
61+
continue;
62+
}
63+
}
64+
65+
key = settings.Inferrer.PropertyName(propertyName);
66+
}
67+
else if (AsType(entry.Key, out indexName))
68+
{
69+
key = settings.Inferrer.IndexName(indexName);
70+
}
71+
else if (AsType(entry.Key, out typeName))
72+
{
73+
key = settings.Inferrer.TypeName(typeName);
74+
}
75+
else
76+
key = Convert.ToString(entry.Key, CultureInfo.InvariantCulture);
77+
78+
if (key != null)
79+
seenEntries[key] = entry.Value;
80+
}
81+
82+
writer.WriteStartObject();
83+
foreach (var entry in seenEntries)
84+
{
85+
writer.WritePropertyName(entry.Key);
86+
serializer.Serialize(writer, entry.Value);
87+
}
88+
writer.WriteEndObject();
89+
}
90+
91+
private static bool AsType<T>(object value, out T convertedValue) where T : class
92+
{
93+
convertedValue = value as T;
94+
return convertedValue != null;
95+
}
96+
}
97+
98+
/// <summary>
99+
/// JSON converter for IDictionary&lt;TKey,TValue&gt; and IReadOnlyDictionary&lt;TKey,TValue&gt;
100+
/// that ignores the contract resolver (e.g. CamelCaseFieldNamesContractResolver)
101+
/// when converting dictionary keys to property names.
102+
/// </summary>
15103
internal class VerbatimDictionaryKeysJsonConverter<TKey, TValue> : JsonConverter
16104
{
17-
public override bool CanConvert(Type t) => typeof (IDictionary<TKey, TValue>).IsAssignableFrom(t);
105+
private readonly bool _keyIsString = typeof(TKey) == typeof(string);
106+
private readonly bool _keyIsField = typeof(TKey) == typeof(Field);
107+
private readonly bool _keyIsPropertyName = typeof(TKey) == typeof(PropertyName);
108+
private readonly bool _keyIsIndexName = typeof(TKey) == typeof(IndexName);
109+
private readonly bool _keyIsTypeName = typeof(TKey) == typeof(TypeName);
110+
111+
public override bool CanConvert(Type t) =>
112+
typeof(IDictionary<TKey, TValue>).IsAssignableFrom(t) ||
113+
typeof(IReadOnlyDictionary<TKey, TValue>).IsAssignableFrom(t);
18114

19115
public override bool CanRead => false;
20116

@@ -25,32 +121,31 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
25121

26122
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
27123
{
28-
var dictionary = value as IDictionary<TKey, TValue>;
29-
if (dictionary == null) return;
124+
var enumerable = (IEnumerable<KeyValuePair<TKey, TValue>>)value;
125+
if (enumerable == null)
126+
{
127+
writer.WriteNull();
128+
return;
129+
}
30130

31131
var settings = serializer.GetConnectionSettings();
32-
var seenEntries = new Dictionary<string, TValue>(dictionary.Count);
33-
var keyIsString = typeof(TKey) == typeof(string);
34-
var keyIsField = typeof(TKey) == typeof(Field);
35-
var keyIsPropertyName = typeof(TKey) == typeof(PropertyName);
36-
var keyIsIndexName = typeof(TKey) == typeof(IndexName);
37-
var keyIsTypeName = typeof(TKey) == typeof(TypeName);
38-
39-
foreach (var entry in dictionary)
132+
var seenEntries = new Dictionary<string, TValue>(enumerable.Count());
133+
134+
foreach (var entry in enumerable)
40135
{
41136
if (entry.Value == null && serializer.NullValueHandling == NullValueHandling.Ignore)
42137
continue;
43138
string key;
44-
if (keyIsString)
139+
if (_keyIsString)
45140
key = entry.Key?.ToString();
46141
else if (settings == null)
47142
key = Convert.ToString(entry.Key, CultureInfo.InvariantCulture);
48-
else if (keyIsField)
143+
else if (_keyIsField)
49144
{
50145
var fieldName = entry.Key as Field;
51146
key = settings.Inferrer.Field(fieldName);
52147
}
53-
else if (keyIsPropertyName)
148+
else if (_keyIsPropertyName)
54149
{
55150
var propertyName = entry.Key as PropertyName;
56151
if (propertyName?.Property != null)
@@ -64,12 +159,12 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
64159

65160
key = settings.Inferrer.PropertyName(propertyName);
66161
}
67-
else if (keyIsIndexName)
162+
else if (_keyIsIndexName)
68163
{
69164
var indexName = entry.Key as IndexName;
70165
key = settings.Inferrer.IndexName(indexName);
71166
}
72-
else if (keyIsTypeName)
167+
else if (_keyIsTypeName)
73168
{
74169
var typeName = entry.Key as TypeName;
75170
key = settings.Inferrer.TypeName(typeName);

src/Nest/CrossPlatform/TypeExtensions.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,35 @@ internal static bool IsGeneric(this Type type)
1313
#if DOTNETCORE
1414
return type.GetTypeInfo().IsGenericType;
1515
#else
16-
return type.IsGenericType;
16+
return type.IsGenericType;
1717
#endif
1818
}
1919

20+
internal static bool IsGenericDictionary(this Type type)
21+
{
22+
return type.GetInterfaces().Any(t =>
23+
t.IsGeneric() && (
24+
t.GetGenericTypeDefinition() == typeof(IDictionary<,>) ||
25+
t.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)));
26+
}
27+
28+
internal static bool TryGetGenericDictionaryArguments(this Type type, out Type[] genericArguments)
29+
{
30+
var genericDictionary = type.GetInterfaces().FirstOrDefault(t =>
31+
t.IsGeneric() && (
32+
t.GetGenericTypeDefinition() == typeof(IDictionary<,>) ||
33+
t.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)));
34+
35+
if (genericDictionary == null)
36+
{
37+
genericArguments = new Type[0];
38+
return false;
39+
}
40+
41+
genericArguments = genericDictionary.GetGenericArguments();
42+
return true;
43+
}
44+
2045
internal static bool IsValue(this Type type)
2146
{
2247
#if DOTNETCORE

0 commit comments

Comments
 (0)