Skip to content
This repository has been archived by the owner on Feb 25, 2021. It is now read-only.

camelCase all the JSONs #746

Merged
merged 6 commits into from
May 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 20 additions & 20 deletions samples/StandaloneApp/wwwroot/sample-data/weather.json
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
[
{
"DateFormatted": "06/05/2018",
"TemperatureC": 1,
"Summary": "Freezing",
"TemperatureF": 33
"dateFormatted": "06/05/2018",
"temperatureC": 1,
"summary": "Freezing",
"temperatureF": 33
},
{
"DateFormatted": "07/05/2018",
"TemperatureC": 14,
"Summary": "Bracing",
"TemperatureF": 57
"dateFormatted": "07/05/2018",
"temperatureC": 14,
"summary": "Bracing",
"temperatureF": 57
},
{
"DateFormatted": "08/05/2018",
"TemperatureC": -13,
"Summary": "Freezing",
"TemperatureF": 9
"dateFormatted": "08/05/2018",
"temperatureC": -13,
"summary": "Freezing",
"temperatureF": 9
},
{
"DateFormatted": "09/05/2018",
"TemperatureC": -16,
"Summary": "Balmy",
"TemperatureF": 4
"dateFormatted": "09/05/2018",
"temperatureC": -16,
"summary": "Balmy",
"temperatureF": 4
},
{
"DateFormatted": "10/05/2018",
"TemperatureC": -2,
"Summary": "Chilly",
"TemperatureF": 29
"dateFormatted": "10/05/2018",
"temperatureC": -2,
"summary": "Chilly",
"temperatureF": 29
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ public class Startup
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver = new DefaultContractResolver();
});
services.AddMvc();

services.AddResponseCompression(options =>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
[
{
"Date": "2018-05-06",
"TemperatureC": 1,
"Summary": "Freezing",
"TemperatureF": 33
"date": "2018-05-06",
"temperatureC": 1,
"summary": "Freezing",
"temperatureF": 33
},
{
"Date": "2018-05-07",
"TemperatureC": 14,
"Summary": "Bracing",
"TemperatureF": 57
"date": "2018-05-07",
"temperatureC": 14,
"summary": "Bracing",
"temperatureF": 57
},
{
"Date": "2018-05-08",
"TemperatureC": -13,
"Summary": "Freezing",
"TemperatureF": 9
"date": "2018-05-08",
"temperatureC": -13,
"summary": "Freezing",
"temperatureF": 9
},
{
"Date": "2018-05-09",
"TemperatureC": -16,
"Summary": "Balmy",
"TemperatureF": 4
"date": "2018-05-09",
"temperatureC": -16,
"summary": "Balmy",
"temperatureF": 4
},
{
"Date": "2018-05-10",
"TemperatureC": -2,
"Summary": "Chilly",
"TemperatureF": 29
"date": "2018-05-10",
"temperatureC": -2,
"summary": "Chilly",
"temperatureF": 29
}
]
59 changes: 59 additions & 0 deletions src/Microsoft.AspNetCore.Blazor/Json/CamelCase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;

namespace Microsoft.AspNetCore.Blazor.Json
{
internal static class CamelCase
{
public static string MemberNameToCamelCase(string value)
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException(
$"The value '{value ?? "null"}' is not a valid member name.",
nameof(value));
}

// If we don't need to modify the value, bail out without creating a char array
if (!char.IsUpper(value[0]))
{
return value;
}

// We have to modify at least one character
var chars = value.ToCharArray();

var length = chars.Length;
if (length < 2 || !char.IsUpper(chars[1]))
{
// Only the first character needs to be modified
// Note that this branch is functionally necessary, because the 'else' branch below
// never looks at char[1]. It's always looking at the n+2 character.
chars[0] = char.ToLowerInvariant(chars[0]);
}
else
{
// If chars[0] and chars[1] are both upper, then we'll lowercase the first char plus
// any consecutive uppercase ones, stopping if we find any char that is followed by a
// non-uppercase one
var i = 0;
while (i < length)
{
chars[i] = char.ToLowerInvariant(chars[i]);

i++;

// If the next-plus-one char isn't also uppercase, then we're now on the last uppercase, so stop
if (i < length - 1 && !char.IsUpper(chars[i + 1]))
{
break;
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the result of camel casing is cached, is there much value in doing perf optimizations here? This only runs once per member per type right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree the value is limited. But it wasn't difficult to implement and doesn't add a lot of complexity. The main benefit vs the Json.NET implementation is that this version only does one IsUpper check per character rather than two. But I agree it's a super-minor benefit.


return new string(chars);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/cc @JamesNK

}
}
}
46 changes: 37 additions & 9 deletions src/Microsoft.AspNetCore.Blazor/Json/SimpleJson/SimpleJson.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1268,7 +1268,7 @@ public PocoJsonSerializerStrategy()

protected virtual string MapClrMemberNameToJsonFieldName(string clrPropertyName)
{
return clrPropertyName;
return CamelCase.MemberNameToCamelCase(clrPropertyName);
}

internal virtual ReflectionUtils.ConstructorDelegate ConstructorDelegateFactory(Type key)
Expand Down Expand Up @@ -1302,23 +1302,51 @@ internal virtual ReflectionUtils.ConstructorDelegate ConstructorDelegateFactory(

internal virtual IDictionary<string, KeyValuePair<Type, ReflectionUtils.SetDelegate>> SetterValueFactory(Type type)
{
IDictionary<string, KeyValuePair<Type, ReflectionUtils.SetDelegate>> result = new Dictionary<string, KeyValuePair<Type, ReflectionUtils.SetDelegate>>();
// BLAZOR-SPECIFIC MODIFICATION FROM STOCK SIMPLEJSON:
//
// For incoming keys we match case-insensitively. But if two .NET properties differ only by case,
// it's ambiguous which should be used: the one that matches the incoming JSON exactly, or the
// one that uses 'correct' PascalCase corresponding to the incoming camelCase? What if neither
// meets these descriptions?
//
// To resolve this:
// - If multiple public properties differ only by case, we throw
// - If multiple public fields differ only by case, we throw
// - If there's a public property and a public field that differ only by case, we prefer the property
// This unambiguously selects one member, and that's what we'll use.

IDictionary<string, KeyValuePair<Type, ReflectionUtils.SetDelegate>> result = new Dictionary<string, KeyValuePair<Type, ReflectionUtils.SetDelegate>>(StringComparer.OrdinalIgnoreCase);
foreach (PropertyInfo propertyInfo in ReflectionUtils.GetProperties(type))
{
if (propertyInfo.CanWrite)
{
MethodInfo setMethod = ReflectionUtils.GetSetterMethodInfo(propertyInfo);
if (setMethod.IsStatic || !setMethod.IsPublic)
continue;
result[MapClrMemberNameToJsonFieldName(propertyInfo.Name)] = new KeyValuePair<Type, ReflectionUtils.SetDelegate>(propertyInfo.PropertyType, ReflectionUtils.GetSetMethod(propertyInfo));
if (result.ContainsKey(propertyInfo.Name))
{
throw new InvalidOperationException($"The type '{type.FullName}' contains multiple public properties with names case-insensitively matching '{propertyInfo.Name.ToLowerInvariant()}'. Such types cannot be used for JSON deserialization.");
}
result[propertyInfo.Name] = new KeyValuePair<Type, ReflectionUtils.SetDelegate>(propertyInfo.PropertyType, ReflectionUtils.GetSetMethod(propertyInfo));
}
}

IDictionary<string, KeyValuePair<Type, ReflectionUtils.SetDelegate>> fieldResult = new Dictionary<string, KeyValuePair<Type, ReflectionUtils.SetDelegate>>(StringComparer.OrdinalIgnoreCase);
foreach (FieldInfo fieldInfo in ReflectionUtils.GetFields(type))
{
if (fieldInfo.IsInitOnly || fieldInfo.IsStatic || !fieldInfo.IsPublic)
continue;
result[MapClrMemberNameToJsonFieldName(fieldInfo.Name)] = new KeyValuePair<Type, ReflectionUtils.SetDelegate>(fieldInfo.FieldType, ReflectionUtils.GetSetMethod(fieldInfo));
if (fieldResult.ContainsKey(fieldInfo.Name))
{
throw new InvalidOperationException($"The type '{type.FullName}' contains multiple public fields with names case-insensitively matching '{fieldInfo.Name.ToLowerInvariant()}'. Such types cannot be used for JSON deserialization.");
}
fieldResult[fieldInfo.Name] = new KeyValuePair<Type, ReflectionUtils.SetDelegate>(fieldInfo.FieldType, ReflectionUtils.GetSetMethod(fieldInfo));
if (!result.ContainsKey(fieldInfo.Name))
{
result[fieldInfo.Name] = fieldResult[fieldInfo.Name];
}
}

return result;
}

Expand Down Expand Up @@ -1435,13 +1463,13 @@ public virtual object DeserializeObject(object value, Type type)
?? throw new InvalidOperationException($"Cannot deserialize JSON into type '{type.FullName}' because it does not have a public parameterless constructor.");
obj = constructorDelegate();

foreach (KeyValuePair<string, KeyValuePair<Type, ReflectionUtils.SetDelegate>> setter in SetCache[type])
var setterCache = SetCache[type];
foreach (var jsonKeyValuePair in jsonObject)
{
object jsonValue;
if (jsonObject.TryGetValue(setter.Key, out jsonValue))
if (setterCache.TryGetValue(jsonKeyValuePair.Key, out var setter))
{
jsonValue = DeserializeObject(jsonValue, setter.Value.Key);
setter.Value.Value(obj, jsonValue);
var jsonValue = DeserializeObject(jsonKeyValuePair.Value, setter.Key);
setter.Value(obj, jsonValue);
}
}
}
Expand Down
Loading