Skip to content

Commit

Permalink
Merge pull request #2987 from snakefoot/JsonDictionaryKeysNonString2
Browse files Browse the repository at this point in the history
JSON encoding should create valid JSON for non-string dictionary-keys
  • Loading branch information
304NotModified committed Nov 2, 2018
2 parents 17da4da + 5345eb5 commit c8cd68f
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 130 deletions.
283 changes: 153 additions & 130 deletions src/NLog/Targets/DefaultJsonSerializer.cs
Expand Up @@ -200,14 +200,14 @@ public bool SerializeObject(object value, StringBuilder destination, JsonSeriali
}
else if (value is IDictionary dict)
{
using (new SingleItemOptimizedHashSet<object>.SingleItemScopedInsert(dict, ref objectsInPath, true))
using (StartScope(ref objectsInPath, dict))
{
SerializeDictionaryObject(dict, destination, options, objectsInPath, depth);
}
}
else if (value is IEnumerable enumerable)
{
using (new SingleItemOptimizedHashSet<object>.SingleItemScopedInsert(value, ref objectsInPath, true))
using (StartScope(ref objectsInPath, value))
{
SerializeCollectionObject(enumerable, destination, options, objectsInPath, depth);
}
Expand Down Expand Up @@ -235,6 +235,11 @@ public bool SerializeObject(object value, StringBuilder destination, JsonSeriali
return true;
}

private static SingleItemOptimizedHashSet<object>.SingleItemScopedInsert StartScope(ref SingleItemOptimizedHashSet<object> objectsInPath, object value)
{
return new SingleItemOptimizedHashSet<object>.SingleItemScopedInsert(value, ref objectsInPath, true);
}

private bool SerializeWithFormatProvider(object value, StringBuilder destination, JsonSerializeOptions options, IFormattable formattable, string format, bool hasFormat)
{
int originalLength = destination.Length;
Expand Down Expand Up @@ -300,35 +305,47 @@ private void SerializeDictionaryObject(IDictionary value, StringBuilder destinat
destination.Append(',');
}

//only serialize, if key and value are serialized without error (e.g. due to reference loop)
if (!SerializeObject(de.Key, destination, options, objectsInPath, nextDepth))
if (options.QuoteKeys)
{
destination.Length = originalLength;
var typeCode = Convert.GetTypeCode(de.Key);
if (!SerializeObjectAsString(de.Key, typeCode, destination, options))
{
destination.Length = originalLength;
continue;
}
}
else
{
if (options.SanitizeDictionaryKeys)
{
int quoteSkipCount = options.QuoteKeys ? 1 : 0;
int keyEndIndex = destination.Length - quoteSkipCount;
int keyStartIndex = originalLength + (first ? 0 : 1) + quoteSkipCount;
if (!SanitizeDictionaryKey(destination, keyStartIndex, keyEndIndex - keyStartIndex))
{
destination.Length = originalLength; // Empty keys are not allowed
continue;
}
}

destination.Append(':');
if (!SerializeObject(de.Value, destination, options, objectsInPath, nextDepth))
if (!SerializeObject(de.Key, destination, options, objectsInPath, nextDepth))
{
destination.Length = originalLength;
continue;
}
else
}

if (options.SanitizeDictionaryKeys)
{
int quoteSkipCount = options.QuoteKeys ? 1 : 0;
int keyEndIndex = destination.Length - quoteSkipCount;
int keyStartIndex = originalLength + (first ? 0 : 1) + quoteSkipCount;
if (!SanitizeDictionaryKey(destination, keyStartIndex, keyEndIndex - keyStartIndex))
{
first = false;
destination.Length = originalLength; // Empty keys are not allowed
continue;
}
}

destination.Append(':');

//only serialize, if key and value are serialized without error (e.g. due to reference loop)
if (!SerializeObject(de.Value, destination, options, objectsInPath, nextDepth))
{
destination.Length = originalLength;
}
else
{
first = false;
}
}
destination.Append('}');
}
Expand Down Expand Up @@ -399,104 +416,101 @@ private bool SerializeTypeCodeValue(object value, StringBuilder destination, Jso
{
//object without property, to string
QuoteValue(destination, Convert.ToString(value, CultureInfo.InvariantCulture));
return true;
}
else if (value is DateTimeOffset)
{
QuoteValue(destination, $"{value:yyyy-MM-dd HH:mm:ss zzz}");
return true;
}
else
{
int originalLength = destination.Length;
if (originalLength > MaxJsonLength)
{
return false;
}

if (depth < options.MaxRecursionLimit)
{
try
{
if (value is Exception && ReferenceEquals(options, instance._serializeOptions))
{
// Exceptions are seldom under control, and can include random Data-Dictionary-keys, so we sanitize by default
options = instance._exceptionSerializeOptions;
}

using (new SingleItemOptimizedHashSet<object>.SingleItemScopedInsert(value, ref objectsInPath, false))
{
return SerializeProperties(value, destination, options, objectsInPath, depth);
}
}
catch
{
//nothing to add, so return is OK
destination.Length = originalLength;
return false;
}
}
else
{
try
{
string str = Convert.ToString(value, CultureInfo.InvariantCulture);
destination.Append('"');
AppendStringEscape(destination, str, options.EscapeUnicode);
destination.Append('"');
}
catch
{
return false;
}
}
return SerializeObjectWithProperties(value, destination, options, ref objectsInPath, depth);
}
}
else
{
if (IsNumericTypeCode(objTypeCode, false))
{
SerializeNumber(value, destination, options, objTypeCode);
}
else
return SerializeSimpleTypeCodeValue(value, objTypeCode, destination, options);
}
}

private bool SerializeObjectWithProperties(object value, StringBuilder destination, JsonSerializeOptions options, ref SingleItemOptimizedHashSet<object> objectsInPath, int depth)
{
int originalLength = destination.Length;
if (originalLength > MaxJsonLength)
{
return false;
}

if (depth < options.MaxRecursionLimit)
{
try
{
string str = XmlHelper.XmlConvertToString(value, objTypeCode);
if (str == null)
if (value is Exception && ReferenceEquals(options, instance._serializeOptions))
{
return false;
// Exceptions are seldom under control, and can include random Data-Dictionary-keys, so we sanitize by default
options = instance._exceptionSerializeOptions;
}
if (SkipQuotes(value, objTypeCode))

using (new SingleItemOptimizedHashSet<object>.SingleItemScopedInsert(value, ref objectsInPath, false))
{
destination.Append(str);
}
else
{
if (objTypeCode == TypeCode.Char)
{
destination.Append('"');
AppendStringEscape(destination, str, options.EscapeUnicode);
destination.Append('"');
}
else
{
QuoteValue(destination, str);
}
return SerializeProperties(value, destination, options, objectsInPath, depth);
}
}
catch
{
//nothing to add, so return is OK
destination.Length = originalLength;
return false;
}
}
else
{
return SerializeObjectAsString(value, TypeCode.Object, destination, options);
}

return true;
}

private void SerializeNumber(object value, StringBuilder destination, JsonSerializeOptions options, TypeCode objTypeCode)
private bool SerializeSimpleTypeCodeValue(object value, TypeCode objTypeCode, StringBuilder destination, JsonSerializeOptions options, bool forceQuotes = false)
{
Enum enumValue;
if (!options.EnumAsInteger && (enumValue = value as Enum) != null)
if (objTypeCode == TypeCode.String || objTypeCode == TypeCode.Char)
{
QuoteValue(destination, EnumAsString(enumValue));
destination.Append('"');
AppendStringEscape(destination, value.ToString(), options.EscapeUnicode);
destination.Append('"');
}
else if (IsNumericTypeCode(objTypeCode, false))
{
if (!options.EnumAsInteger && value is Enum enumValue)
{
QuoteValue(destination, EnumAsString(enumValue));
}
else
{
if (forceQuotes)
destination.Append('"');
destination.AppendIntegerAsString(value, objTypeCode);
if (forceQuotes)
destination.Append('"');
}
}
else
{
destination.AppendIntegerAsString(value, objTypeCode);
string str = XmlHelper.XmlConvertToString(value, objTypeCode);
if (str == null)
{
return false;
}

if (!forceQuotes && SkipQuotes(value, objTypeCode))
{
destination.Append(str);
}
else
{
QuoteValue(destination, str);
}
}
return true;
}

private static CultureInfo CreateFormatProvider()
Expand Down Expand Up @@ -541,30 +555,27 @@ private string EnumAsString(Enum value)
/// </summary>
private static bool SkipQuotes(object value, TypeCode objTypeCode)
{
if (objTypeCode != TypeCode.String)
switch (objTypeCode)
{
if (objTypeCode == TypeCode.Empty || objTypeCode == TypeCode.Boolean)
return true; // Don't put quotes around null values

if (IsNumericTypeCode(objTypeCode, false) || objTypeCode == TypeCode.Decimal)
return true;

if (objTypeCode == TypeCode.Double)
{
double dblValue = (double)value;
if (!double.IsNaN(dblValue) && !double.IsInfinity(dblValue))
return true;
}

if (objTypeCode == TypeCode.Single)
{
float floatValue = (float)value;
if (!float.IsNaN(floatValue) && !float.IsInfinity(floatValue))
return true;
}
case TypeCode.String: return false;
case TypeCode.Char: return false;
case TypeCode.DateTime: return false;
case TypeCode.Empty: return true;
case TypeCode.Boolean: return true;
case TypeCode.Decimal: return true;
case TypeCode.Double:
{
double dblValue = (double)value;
return !double.IsNaN(dblValue) && !double.IsInfinity(dblValue);
}
case TypeCode.Single:
{
float floatValue = (float)value;
return !float.IsNaN(floatValue) && !float.IsInfinity(floatValue);
}
default:
return IsNumericTypeCode(objTypeCode, false);
}

return false;
}

/// <summary>
Expand Down Expand Up @@ -705,19 +716,8 @@ private static bool EscapeChar(char ch, bool escapeUnicode)
var props = GetProps(value);
if (props.Key.Length == 0)
{
try
{
//no props
var str = Convert.ToString(value, CultureInfo.InvariantCulture);
destination.Append('"');
AppendStringEscape(destination, str, options.EscapeUnicode);
destination.Append('"');
return true;
}
catch
{
return false;
}
//no props
return SerializeObjectAsString(value, TypeCode.Object, destination, options);
}

destination.Append('{');
Expand Down Expand Up @@ -771,6 +771,29 @@ private static bool EscapeChar(char ch, bool escapeUnicode)
return true;
}

private bool SerializeObjectAsString(object value, TypeCode objTypeCode, StringBuilder destination, JsonSerializeOptions options)
{
try
{
if (objTypeCode == TypeCode.Object)
{
var str = Convert.ToString(value, CultureInfo.InvariantCulture);
destination.Append('"');
AppendStringEscape(destination, str, options.EscapeUnicode);
destination.Append('"');
return true;
}
else
{
return SerializeSimpleTypeCodeValue(value, objTypeCode, destination, options, true);
}
}
catch
{
return false;
}
}

/// <summary>
/// Get properties, cached for a type
/// </summary>
Expand All @@ -779,13 +802,13 @@ private static bool EscapeChar(char ch, bool escapeUnicode)
private KeyValuePair<PropertyInfo[], ReflectionHelpers.LateBoundMethod[]> GetProps(object value)
{
var type = value.GetType();
KeyValuePair<PropertyInfo[],ReflectionHelpers.LateBoundMethod[]> props;
KeyValuePair<PropertyInfo[], ReflectionHelpers.LateBoundMethod[]> props;
if (_propsCache.TryGetValue(type, out props))
{
if (props.Key.Length != 0 && props.Value.Length == 0)
{
var lateBoundMethods = new ReflectionHelpers.LateBoundMethod[props.Key.Length];
for(int i = 0; i < props.Key.Length; i++)
for (int i = 0; i < props.Key.Length; i++)
{
lateBoundMethods[i] = ReflectionHelpers.CreateLateBoundMethod(props.Key[i].GetGetMethod());
}
Expand Down

0 comments on commit c8cd68f

Please sign in to comment.