camelCase all the JSONs (#746)
* Add camelCase utility * Use camelCase when JSON-serializing (but not for dictionary keys) * Make JSON deserialization treat member names case-insensitively (but retain case on dictionary keys) * Use camelCase in JSON in the samples and templates * Reverse the order of the params for the camelcase test because it's weird otherwise * CR feedback
This commit is contained in:
parent
f9ab9cc6e5
commit
20e43adac5
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -18,10 +18,7 @@ namespace BlazorHosted_CSharp.Server
|
|||
// 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 =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new string(chars);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1268,7 +1268,7 @@ namespace SimpleJson
|
|||
|
||||
protected virtual string MapClrMemberNameToJsonFieldName(string clrPropertyName)
|
||||
{
|
||||
return clrPropertyName;
|
||||
return CamelCase.MemberNameToCamelCase(clrPropertyName);
|
||||
}
|
||||
|
||||
internal virtual ReflectionUtils.ConstructorDelegate ConstructorDelegateFactory(Type key)
|
||||
|
|
@ -1302,7 +1302,20 @@ namespace SimpleJson
|
|||
|
||||
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)
|
||||
|
|
@ -1310,15 +1323,30 @@ namespace SimpleJson
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
@ -1435,13 +1463,13 @@ namespace SimpleJson
|
|||
?? 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,12 +53,13 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
Hobby = Hobbies.Swordfighting,
|
||||
Nicknames = new List<string> { "Comte de la Fère", "Armand" },
|
||||
BirthInstant = new DateTimeOffset(1825, 8, 6, 18, 45, 21, TimeSpan.FromHours(-6)),
|
||||
Age = new TimeSpan(7665, 1, 30, 0)
|
||||
Age = new TimeSpan(7665, 1, 30, 0),
|
||||
Allergies = new Dictionary<string, object> { { "Ducks", true }, { "Geese", false } },
|
||||
};
|
||||
|
||||
// Act/Assert
|
||||
Assert.Equal(
|
||||
"{\"Id\":1844,\"Name\":\"Athos\",\"Pets\":[\"Aramis\",\"Porthos\",\"D'Artagnan\"],\"Hobby\":2,\"Nicknames\":[\"Comte de la Fère\",\"Armand\"],\"BirthInstant\":\"1825-08-06T18:45:21.0000000-06:00\",\"Age\":\"7665.01:30:00\"}",
|
||||
"{\"id\":1844,\"name\":\"Athos\",\"pets\":[\"Aramis\",\"Porthos\",\"D'Artagnan\"],\"hobby\":2,\"nicknames\":[\"Comte de la Fère\",\"Armand\"],\"birthInstant\":\"1825-08-06T18:45:21.0000000-06:00\",\"age\":\"7665.01:30:00\",\"allergies\":{\"Ducks\":true,\"Geese\":false}}",
|
||||
JsonUtil.Serialize(person));
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +67,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
public void CanDeserializeClassFromJson()
|
||||
{
|
||||
// Arrange
|
||||
var json = "{\"Id\":1844,\"Name\":\"Athos\",\"Pets\":[\"Aramis\",\"Porthos\",\"D'Artagnan\"],\"Hobby\":2,\"Nicknames\":[\"Comte de la Fère\",\"Armand\"],\"BirthInstant\":\"1825-08-06T18:45:21.0000000-06:00\",\"Age\":\"7665.01:30:00\"}";
|
||||
var json = "{\"id\":1844,\"name\":\"Athos\",\"pets\":[\"Aramis\",\"Porthos\",\"D'Artagnan\"],\"hobby\":2,\"nicknames\":[\"Comte de la Fère\",\"Armand\"],\"birthInstant\":\"1825-08-06T18:45:21.0000000-06:00\",\"age\":\"7665.01:30:00\",\"allergies\":{\"Ducks\":true,\"Geese\":false}}";
|
||||
|
||||
// Act
|
||||
var person = JsonUtil.Deserialize<Person>(json);
|
||||
|
|
@ -79,6 +80,35 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
Assert.Equal(new[] { "Comte de la Fère", "Armand" }, person.Nicknames);
|
||||
Assert.Equal(new DateTimeOffset(1825, 8, 6, 18, 45, 21, TimeSpan.FromHours(-6)), person.BirthInstant);
|
||||
Assert.Equal(new TimeSpan(7665, 1, 30, 0), person.Age);
|
||||
Assert.Equal(new Dictionary<string, object> { { "Ducks", true }, { "Geese", false } }, person.Allergies);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanDeserializeWithCaseInsensitiveKeys()
|
||||
{
|
||||
// Arrange
|
||||
var json = "{\"ID\":1844,\"NamE\":\"Athos\"}";
|
||||
|
||||
// Act
|
||||
var person = JsonUtil.Deserialize<Person>(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1844, person.Id);
|
||||
Assert.Equal("Athos", person.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeserializationPrefersPropertiesOverFields()
|
||||
{
|
||||
// Arrange
|
||||
var json = "{\"member1\":\"Hello\"}";
|
||||
|
||||
// Act
|
||||
var person = JsonUtil.Deserialize<PrefersPropertiesOverFields>(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Hello", person.Member1);
|
||||
Assert.Null(person.member1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -96,14 +126,14 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
var result = JsonUtil.Serialize(commandResult);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("{\"StringProperty\":\"Test\",\"BoolProperty\":true,\"NullableIntProperty\":1}", result);
|
||||
Assert.Equal("{\"stringProperty\":\"Test\",\"boolProperty\":true,\"nullableIntProperty\":1}", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanDeserializeStructFromJson()
|
||||
{
|
||||
// Arrange
|
||||
var json = "{\"StringProperty\":\"Test\",\"BoolProperty\":true,\"NullableIntProperty\":1}";
|
||||
var json = "{\"stringProperty\":\"Test\",\"boolProperty\":true,\"nullableIntProperty\":1}";
|
||||
|
||||
//Act
|
||||
var simpleError = JsonUtil.Deserialize<SimpleStruct>(json);
|
||||
|
|
@ -114,6 +144,34 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
Assert.Equal(1, simpleError.NullableIntProperty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectsTypesWithAmbiguouslyNamedProperties()
|
||||
{
|
||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
JsonUtil.Deserialize<ClashingProperties>("{}");
|
||||
});
|
||||
|
||||
Assert.Equal($"The type '{typeof(ClashingProperties).FullName}' contains multiple public properties " +
|
||||
$"with names case-insensitively matching '{nameof(ClashingProperties.PROP1).ToLowerInvariant()}'. " +
|
||||
$"Such types cannot be used for JSON deserialization.",
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectsTypesWithAmbiguouslyNamedFields()
|
||||
{
|
||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
JsonUtil.Deserialize<ClashingFields>("{}");
|
||||
});
|
||||
|
||||
Assert.Equal($"The type '{typeof(ClashingFields).FullName}' contains multiple public fields " +
|
||||
$"with names case-insensitively matching '{nameof(ClashingFields.Field1).ToLowerInvariant()}'. " +
|
||||
$"Such types cannot be used for JSON deserialization.",
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NonEmptyConstructorThrowsUsefulException()
|
||||
{
|
||||
|
|
@ -143,6 +201,50 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
Assert.Equal("{\"key1\":\"value1\",\"key2\":123}", json);
|
||||
}
|
||||
|
||||
// Test cases based on https://github.com/JamesNK/Newtonsoft.Json/blob/122afba9908832bd5ac207164ee6c303bfd65cf1/Src/Newtonsoft.Json.Tests/Utilities/StringUtilsTests.cs#L41
|
||||
// The only difference is that our logic doesn't have to handle space-separated words,
|
||||
// because we're only use this for camelcasing .NET member names
|
||||
//
|
||||
// Not all of the following cases are really valid .NET member names, but we have no reason
|
||||
// to implement more logic to detect invalid member names besides the basics (null or empty).
|
||||
[Theory]
|
||||
[InlineData("URLValue", "urlValue")]
|
||||
[InlineData("URL", "url")]
|
||||
[InlineData("ID", "id")]
|
||||
[InlineData("I", "i")]
|
||||
[InlineData("Person", "person")]
|
||||
[InlineData("xPhone", "xPhone")]
|
||||
[InlineData("XPhone", "xPhone")]
|
||||
[InlineData("X_Phone", "x_Phone")]
|
||||
[InlineData("X__Phone", "x__Phone")]
|
||||
[InlineData("IsCIA", "isCIA")]
|
||||
[InlineData("VmQ", "vmQ")]
|
||||
[InlineData("Xml2Json", "xml2Json")]
|
||||
[InlineData("SnAkEcAsE", "snAkEcAsE")]
|
||||
[InlineData("SnA__kEcAsE", "snA__kEcAsE")]
|
||||
[InlineData("already_snake_case_", "already_snake_case_")]
|
||||
[InlineData("IsJSONProperty", "isJSONProperty")]
|
||||
[InlineData("SHOUTING_CASE", "shoutinG_CASE")]
|
||||
[InlineData("9999-12-31T23:59:59.9999999Z", "9999-12-31T23:59:59.9999999Z")]
|
||||
[InlineData("Hi!! This is text. Time to test.", "hi!! This is text. Time to test.")]
|
||||
[InlineData("BUILDING", "building")]
|
||||
[InlineData("BUILDINGProperty", "buildingProperty")]
|
||||
public void MemberNameToCamelCase_Valid(string input, string expectedOutput)
|
||||
{
|
||||
Assert.Equal(expectedOutput, CamelCase.MemberNameToCamelCase(input));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(null)]
|
||||
public void MemberNameToCamelCase_Invalid(string input)
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
CamelCase.MemberNameToCamelCase(input));
|
||||
Assert.Equal("value", ex.ParamName);
|
||||
Assert.StartsWith($"The value '{input ?? "null"}' is not a valid member name.", ex.Message);
|
||||
}
|
||||
|
||||
class NonEmptyConstructorPoco
|
||||
{
|
||||
public NonEmptyConstructorPoco(int parameter) {}
|
||||
|
|
@ -166,6 +268,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
public IList<string> Nicknames { get; set; }
|
||||
public DateTimeOffset BirthInstant { get; set; }
|
||||
public TimeSpan Age { get; set; }
|
||||
public IDictionary<string, object> Allergies { get; set; }
|
||||
}
|
||||
|
||||
enum Hobbies { Reading = 1, Swordfighting = 2 }
|
||||
|
|
@ -181,5 +284,25 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable 0649
|
||||
class ClashingProperties
|
||||
{
|
||||
public string Prop1 { get; set; }
|
||||
public int PROP1 { get; set; }
|
||||
}
|
||||
|
||||
class ClashingFields
|
||||
{
|
||||
public string Field1;
|
||||
public int field1;
|
||||
}
|
||||
|
||||
class PrefersPropertiesOverFields
|
||||
{
|
||||
public string member1;
|
||||
public string Member1 { get; set; }
|
||||
}
|
||||
#pragma warning restore 0649
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue