Skip to content

Commit 278553e

Browse files
committed
Json.NET 5.0.8 is unable to (directly) deserialize a null JToken value in the extension dictionary (fixes #419)
1 parent ac910fd commit 278553e

1 file changed

Lines changed: 272 additions & 1 deletion

File tree

src/corelib/Core/Domain/ExtensibleJsonObject.cs

Lines changed: 272 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
{
33
using System;
44
using System.Collections.Generic;
5+
using System.Diagnostics;
56
using System.Linq;
67
using net.openstack.Core.Collections;
78
using Newtonsoft.Json;
89
using Newtonsoft.Json.Linq;
10+
using IEnumerable = System.Collections.IEnumerable;
11+
using IEnumerator = System.Collections.IEnumerator;
912

1013
/// <summary>
1114
/// This is the abstract base class for types modeling the JSON representation of a resource
@@ -27,7 +30,6 @@ public abstract class ExtensibleJsonObject
2730
/// <summary>
2831
/// This is the backing field for the <see cref="ExtensionData"/> property.
2932
/// </summary>
30-
[JsonExtensionData]
3133
private Dictionary<string, JToken> _extensionData;
3234

3335
/// <summary>
@@ -113,5 +115,274 @@ public ReadOnlyDictionary<string, JToken> ExtensionData
113115
return new ReadOnlyDictionary<string, JToken>(_extensionData);
114116
}
115117
}
118+
119+
/// <summary>
120+
/// This property exposes the <see cref="_extensionData"/> field to Json.NET as a dictionary with
121+
/// <see cref="object"/> values instead of <see cref="JToken"/> values, which works around a known bug in the
122+
/// way Json.NET 5.x handled <see langword="null"/> values in the extension data.
123+
/// </summary>
124+
[JsonExtensionData]
125+
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
126+
private ExtensionDataDictionary ExtensionDataWrapper
127+
{
128+
get
129+
{
130+
// This can never return null or Json.NET will attempt to set the value.
131+
return new ExtensionDataDictionary(this);
132+
}
133+
134+
set
135+
{
136+
// This setter must exist or Json.NET will not recognize the extension data. It cannot be used because
137+
// Json.NET will bypass the getter, resulting in a lost update.
138+
throw new NotSupportedException("Attempted to set the extension data wrapper. See issue openstacknetsdk/openstack.net#419.");
139+
}
140+
}
141+
142+
/// <summary>
143+
/// Converts an object to a <see cref="JToken"/>.
144+
/// </summary>
145+
/// <remarks>
146+
/// <para>
147+
/// Unlike <see cref="JToken.FromObject(object)"/>, this method supports <see langword="null"/> values.
148+
/// </para>
149+
/// </remarks>
150+
/// <param name="obj">The object.</param>
151+
/// <returns>
152+
/// <para>The result of calling <see cref="JToken.FromObject(object)"/> on the input object.</para>
153+
/// <para>-or-</para>
154+
/// <para><see langword="null"/> if <paramref name="obj"/> is <see langword="null"/>.</para>
155+
/// </returns>
156+
private static JToken ToJToken(object obj)
157+
{
158+
if (obj == null)
159+
return null;
160+
161+
return JToken.FromObject(obj);
162+
}
163+
164+
/// <summary>
165+
/// This class works around a known bug in Json.NET's handling of JSON extension data.
166+
/// </summary>
167+
/// <remarks>
168+
/// <para>Adding values to the underlying dictionary requires converting the value to a <see cref="JToken"/> by
169+
/// calling <see cref="ToJToken(object)"/>. Reading values does not require the inverse because the serializer
170+
/// in Json.NET has no trouble handling <see cref="JToken"/> values as input.</para>
171+
/// </remarks>
172+
/// <seealso cref="ExtensionDataWrapper"/>
173+
private sealed class ExtensionDataDictionary : IDictionary<string, object>
174+
{
175+
private readonly ExtensibleJsonObject _underlying;
176+
177+
[JsonConstructor]
178+
private ExtensionDataDictionary()
179+
{
180+
// This constructor must exist or Json.NET will not be able to set the extension data. It cannot be used
181+
// because Json.NET will not set the required _underlying field.
182+
throw new NotSupportedException("Attempted to create the extension data wrapper with its underlying object. See issue openstacknetsdk/openstack.net#419.");
183+
}
184+
185+
public ExtensionDataDictionary(ExtensibleJsonObject extensibleJsonObject)
186+
{
187+
if (extensibleJsonObject == null)
188+
throw new ArgumentNullException("extensibleJsonObject");
189+
190+
_underlying = extensibleJsonObject;
191+
}
192+
193+
public object this[string key]
194+
{
195+
get
196+
{
197+
return _underlying.ExtensionData[key];
198+
}
199+
200+
set
201+
{
202+
GetOrCreateExtensionData()[key] = ToJToken(value);
203+
}
204+
}
205+
206+
public int Count
207+
{
208+
get
209+
{
210+
return _underlying.ExtensionData.Count;
211+
}
212+
}
213+
214+
public bool IsReadOnly
215+
{
216+
get
217+
{
218+
return false;
219+
}
220+
}
221+
222+
public ICollection<string> Keys
223+
{
224+
get
225+
{
226+
return _underlying.ExtensionData.Keys;
227+
}
228+
}
229+
230+
public ICollection<object> Values
231+
{
232+
get
233+
{
234+
return new ExtensionDataValues(_underlying.ExtensionData.Values);
235+
}
236+
}
237+
238+
public void Add(KeyValuePair<string, object> item)
239+
{
240+
IDictionary<string, JToken> extensionData = GetOrCreateExtensionData();
241+
extensionData.Add(new KeyValuePair<string, JToken>(item.Key, ToJToken(item.Value)));
242+
}
243+
244+
public void Add(string key, object value)
245+
{
246+
GetOrCreateExtensionData().Add(key, ToJToken(value));
247+
}
248+
249+
public void Clear()
250+
{
251+
GetOrCreateExtensionData().Clear();
252+
}
253+
254+
public bool Contains(KeyValuePair<string, object> item)
255+
{
256+
return _underlying.ExtensionData.Contains(new KeyValuePair<string, JToken>(item.Key, ToJToken(item.Value)));
257+
}
258+
259+
public bool ContainsKey(string key)
260+
{
261+
return _underlying.ExtensionData.ContainsKey(key);
262+
}
263+
264+
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
265+
{
266+
IDictionary<string, object> intermediate = new Dictionary<string, object>(_underlying.ExtensionData.ToDictionary(i => i.Key, i => (object)i.Value));
267+
intermediate.CopyTo(array, arrayIndex);
268+
}
269+
270+
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
271+
{
272+
return _underlying.ExtensionData.Select(i => new KeyValuePair<string, object>(i.Key, i.Value)).GetEnumerator();
273+
}
274+
275+
public bool Remove(KeyValuePair<string, object> item)
276+
{
277+
IDictionary<string, JToken> extensionData = _underlying._extensionData;
278+
if (extensionData == null)
279+
return false;
280+
281+
return extensionData.Remove(new KeyValuePair<string, JToken>(item.Key, ToJToken(item.Value)));
282+
}
283+
284+
public bool Remove(string key)
285+
{
286+
var extensionData = _underlying._extensionData;
287+
if (extensionData == null)
288+
return false;
289+
290+
return extensionData.Remove(key);
291+
}
292+
293+
public bool TryGetValue(string key, out object value)
294+
{
295+
JToken intermediate;
296+
bool result = _underlying.ExtensionData.TryGetValue(key, out intermediate);
297+
value = intermediate;
298+
return result;
299+
}
300+
301+
IEnumerator IEnumerable.GetEnumerator()
302+
{
303+
return GetEnumerator();
304+
}
305+
306+
private Dictionary<string, JToken> GetOrCreateExtensionData()
307+
{
308+
var result = _underlying._extensionData;
309+
if (result == null)
310+
{
311+
result = new Dictionary<string, JToken>();
312+
_underlying._extensionData = result;
313+
}
314+
315+
return result;
316+
}
317+
}
318+
319+
/// <summary>
320+
/// This class works around a known bug in Json.NET's handling of JSON extension data.
321+
/// </summary>
322+
/// <seealso cref="ExtensionDataWrapper"/>
323+
private class ExtensionDataValues : ICollection<object>
324+
{
325+
private readonly ICollection<JToken> _values;
326+
327+
public ExtensionDataValues(ICollection<JToken> values)
328+
{
329+
if (values == null)
330+
throw new ArgumentNullException("values");
331+
332+
_values = values;
333+
}
334+
335+
public int Count
336+
{
337+
get
338+
{
339+
return _values.Count;
340+
}
341+
}
342+
343+
public bool IsReadOnly
344+
{
345+
get
346+
{
347+
return _values.IsReadOnly;
348+
}
349+
}
350+
351+
public void Add(object item)
352+
{
353+
_values.Add(ToJToken(item));
354+
}
355+
356+
public void Clear()
357+
{
358+
_values.Clear();
359+
}
360+
361+
public bool Contains(object item)
362+
{
363+
return _values.Contains(ToJToken(item));
364+
}
365+
366+
public void CopyTo(object[] array, int arrayIndex)
367+
{
368+
ICollection<object> intermediate = _values.ToArray();
369+
intermediate.CopyTo(array, arrayIndex);
370+
}
371+
372+
public IEnumerator<object> GetEnumerator()
373+
{
374+
return _values.Cast<object>().GetEnumerator();
375+
}
376+
377+
public bool Remove(object item)
378+
{
379+
return _values.Remove(ToJToken(item));
380+
}
381+
382+
IEnumerator IEnumerable.GetEnumerator()
383+
{
384+
return GetEnumerator();
385+
}
386+
}
116387
}
117388
}

0 commit comments

Comments
 (0)