How to Apply a General Rule for Remapping All Property Names When Serializing with JSON.Net

How to apply a general rule for remapping all property names when serializing with Json.NET?

Assuming you are working with Json.NET 9.0.1 or later, this can be done with a custom NamingStrategy. For instance, here's one based on SnakeCaseNamingStrategy and StringUtils.ToSnakeCase() by James Newton-King:

public class CustomNamingStrategy : NamingStrategy
{
public CustomNamingStrategy(bool processDictionaryKeys, bool overrideSpecifiedNames)
{
ProcessDictionaryKeys = processDictionaryKeys;
OverrideSpecifiedNames = overrideSpecifiedNames;
}

public CustomNamingStrategy(bool processDictionaryKeys, bool overrideSpecifiedNames, bool processExtensionDataNames)
: this(processDictionaryKeys, overrideSpecifiedNames)
{
ProcessExtensionDataNames = processExtensionDataNames;
}

public CustomNamingStrategy()
{
}

protected override string ResolvePropertyName(string name)
{
return SpaceWords(name);
}

enum WordState
{
Start,
Lower,
Upper,
NewWord
}

static string SpaceWords(string s)
{
// Adapted from StringUtils.ToSnakeCase()
// https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Utilities/StringUtils.cs#L191
//
// Copyright (c) 2007 James Newton-King
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.

char wordBreakChar = ' ';

if (string.IsNullOrEmpty(s))
{
return s;
}

StringBuilder sb = new StringBuilder();
WordState state = WordState.Start;

for (int i = 0; i < s.Length; i++)
{
if (s[i] == ' ')
{
if (state != WordState.Start)
{
state = WordState.NewWord;
}
}
else if (char.IsUpper(s[i]))
{
switch (state)
{
case WordState.Upper:
bool hasNext = (i + 1 < s.Length);
if (i > 0 && hasNext)
{
char nextChar = s[i + 1];
if (!char.IsUpper(nextChar) && nextChar != ' ')
{
sb.Append(wordBreakChar);
}
}
break;
case WordState.Lower:
case WordState.NewWord:
sb.Append(wordBreakChar);
break;
}

sb.Append(s[i]);

state = WordState.Upper;
}
else if (s[i] == wordBreakChar)
{
sb.Append(wordBreakChar);
state = WordState.Start;
}
else
{
if (state == WordState.NewWord)
{
sb.Append(wordBreakChar);
}

sb.Append(s[i]);
state = WordState.Lower;
}
}

sb.Replace("Number", "#");
return sb.ToString();
}
}

Then you can apply it to your type as follows:

[JsonObject(NamingStrategyType = typeof(CustomNamingStrategy))]
public class RootObject
{
public string JobType { get; set; }

public string JobNumber { get; set; }

public int JobItemCount { get; set; }

public string ISOCode { get; set; }

public string SourceXML { get; set; }
}

And the JSON generated will be as follows:

{
"Job Type": "job type",
"Job #": "01010101",
"Job Item Count": 3,
"ISO Code": "ISO 9000",
"Source XML": "c:\temp.xml"
}

Notes:

  • If you want the strategy to apply to properties that already have property names specified via JsonPropertyAttribute.PropertyName, set NamingStrategy.OverrideSpecifiedNames == true.

  • To apply your naming strategy to all types rather than setting it on each object, you can set the naming strategy in DefaultContractResolver.NamingStrategy, then set the contract resolver in JsonSerializerSettings.ContractResolver.

  • The naming strategy maps from the c# property name to the JSON property name, not vice versa. Thus you need to insert spaces rather than "pluck them out" and replace "Number" with "#". The mapping is then cached by the contract resolver and a reverse lookup is done during deserialization.

Issue with Default camelCase serialization of All Caps property names to JSON in ASP.Net core

Json.NET's CamelCasePropertyNamesContractResolver uses a CamelCaseNamingStrategy to convert the property names to camelcase. Internally it uses StringUtils.ToCamelCase which doesn't convert a character to lowercase in case it is followed by a number, see link.

CamelCaseNamingStrategy

public class CamelCaseNamingStrategy : NamingStrategy
{
// ...

protected override string ResolvePropertyName(string name)
{
return StringUtils.ToCamelCase(name);
}
}

StringUtils

Notice the 2nd if statement, where there's no check for a number.

internal static class StringUtils
{
public static string ToCamelCase(string s)
{
if (!string.IsNullOrEmpty(s) && char.IsUpper(s[0]))
{
char[] array = s.ToCharArray();
for (int i = 0; i < array.Length && (i != 1 || char.IsUpper(array[i])); i++)
{
bool flag = i + 1 < array.Length;
if ((i > 0 & flag) && !char.IsUpper(array[i + 1])) // << Missing check for a number.
{
break;
}
char c = char.ToLower(array[i], CultureInfo.InvariantCulture);
array[i] = c;
}
return new string(array);
}
return s;
}
}

You can implement a custom NamingStrategy to implement this missing check as shown below.

class CustomCamelCaseNamingStrategy : CamelCaseNamingStrategy
{
protected override String ResolvePropertyName(String propertyName)
{
return this.toCamelCase(propertyName);
}

private string toCamelCase(string s)
{
if (!string.IsNullOrEmpty(s) && char.IsUpper(s[0]))
{
char[] array = s.ToCharArray();
for (int i = 0; i < array.Length && (i != 1 || char.IsUpper(array[i])); i++)
{
bool flag = i + 1 < array.Length;
if ((i > 0 & flag) && !char.IsUpper(array[i + 1]) && !char.IsNumber(array[i + 1]))
{
break;
}
char c = char.ToLower(array[i], CultureInfo.InvariantCulture);
array[i] = c;
}
return new string(array);
}
return s;
}
}

In ConfigureServices you assign this custom NamingStrategy to the CamelCasePropertyNamesContractResolver.

There's no need to implement a full custom ContractResolver.

(When using the default CamelCaseNamingStrategy, the CamelCasePropertyNamesContractResolver sets the properties ProcessDictionaryKeys and OverrideSpecifiedNames to True, so we keep this behaviour.)

services
.AddMvc()
.AddJsonOptions(options =>
options.SerializerSettings.ContractResolver =
new CamelCasePropertyNamesContractResolver() {
NamingStrategy = new CustomCamelCaseNamingStrategy() {
ProcessDictionaryKeys = true,
OverrideSpecifiedNames = true
}});

How do I remap names to object properties when deserializing, using Json.Net in C#

You can use the JsonProperty attribute.

[JsonProperty("foo")]
public string A { get; set; }

Newtonsoft.Json replacing spaces with underscores C# to JSON Serialize

PROBLEM SOLVED:

Found this wonderful article in the meantime. Upon that I just implemented functions from the article, modified Replace in ResolvePropertyName and voila, my code is working splendidly! Hope this helps everyone else who is using Entity Framework, generating model from database and trying to go from C# objects to JSON.

public class JSON
{
public static string ConvertEntityToJSON(object dataToSerialize)
{
return JsonConvert.SerializeObject(dataToSerialize,
new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new UnderscorePropertyNamesContractResolver()
});
}
}

public class UnderscorePropertyNamesContractResolver : DefaultContractResolver
{
public UnderscorePropertyNamesContractResolver() : base()
{
}

protected override string ResolvePropertyName(string propertyName)
{
return Regex.Replace(propertyName, "_", " ");
}
}

Deserialize nested properties using Json.Net without using data annotations

I don't think the resolver idea is going to work because you are remapping more than just property names -- you are also trying to deserialize into a class structure that doesn't always match the shape of JSON. This job is better suited for a set of JsonConverters.

Here's the basic approach:

  1. Create one JsonConverter for each model class for which the JSON varies.
  2. Inside the ReadJson method load a JObject from the reader.
  3. Detect which format you have by looking for well-known property names which are always present for that format. For example, if you can rely on event_id always being present in the first format, that is a good way to detect it, because you know the second format does not have that property. You can base this check on the presence of multiple properties if needed; the key is just to use some combination that appears in only one format and no others. (Or if you know ahead of time which format to expect, you can simply parameterize the converters, i.e. pass a format flag in the constructor.)
  4. Once the format is known, populate the model from the JObject.

For the Event model shown in your question, the converter might look something like this:

public class EventConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Event);
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
Event evt = new Event();
JObject obj = JObject.Load(reader);
if (obj["event_id"] != null)
{
// JSON format #1
evt.EventId = (int)obj["event_id"];
evt.EventName = (string)obj["event_name"];
evt.StartDate = (DateTime)obj["start_date"];
evt.EndDate = (DateTime)obj["end_date"];
evt.Guests = obj.SelectToken("participants.guests").ToObject<List<Guest>>(serializer);
}
else if (obj["name"] != null)
{
// JSON format #2
evt.EventName = (string)obj["name"];
evt.StartDate = (DateTime)obj["from"];
evt.EndDate = (DateTime)obj["to"];
evt.Guests = obj["guests"].ToObject<List<Guest>>(serializer);
}
else
{
throw new JsonException("Unknown format for Event");
}
return evt;
}

public override bool CanWrite
{
get { return false; }
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}

Similarly for the Guest model, we might have this JsonConverter:

public class GuestConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Guest);
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
Guest guest = new Guest();
JObject obj = JObject.Load(reader);
if (obj["guest_id"] != null)
{
// JSON format #1
guest.GuestId = (string)obj["guest_id"];
guest.FirstName = (string)obj["first_name"];
guest.LastName = (string)obj["last_name"];
}
else if (obj["email"] != null)
{
// JSON format #2
guest.FirstName = (string)obj["firstName"];
guest.LastName = (string)obj["lastName"];
guest.Email = (string)obj["email"];
}
else
{
throw new JsonException("Unknown format for Guest");
}
return guest;
}

public override bool CanWrite
{
get { return false; }
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}

To use the converters, add them to the Converters collection of the JsonSerializerSettings object and pass the settings to DeserializeObject() like this:

var settings = new JsonSerializerSettings
{
Converters = new List<JsonConverter> { new EventConverter(), new GuestConverter() }
};

var evt = JsonConvert.DeserializeObject<Event>(json, settings);

Demo fiddle: https://dotnetfiddle.net/KI82KB

How to use JSON.Net CamelCasePropertyNamesContractResolver with dot separated names?

Assuming that the validation errors are serialized as some sort of IDictionary<string, T> for some T, then the JSON property names corresponding to the dictionary keys can be piecewise camel-cased between each . character by creating a custom naming strategy.

Json.NET encapsulates the logic to algorithmically remap property names and dictionary keys (e.g. to camel case) in the NamingStrategy type, specifically CamelCaseNamingStrategy. To modify the logic of a naming strategy to apply to each portion of a property name between . characters, you can adopt the decorator pattern and create a decorator naming strategy that applies some inner strategy to each portion of the name like so:

public class PiecewiseNamingStrategy : NamingStrategy
{
readonly NamingStrategy baseStrategy;

public PiecewiseNamingStrategy(NamingStrategy baseStrategy)
{
if (baseStrategy == null)
throw new ArgumentNullException();
this.baseStrategy = baseStrategy;
}

protected override string ResolvePropertyName(string name)
{
return String.Join(".", name.Split('.').Select(n => baseStrategy.GetPropertyName(n, false)));
}
}

Then, configure MVC as follows:

options.ContractResolver = new DefaultContractResolver
{
NamingStrategy = new PiecewiseNamingStrategy(new CamelCaseNamingStrategy())
{
OverrideSpecifiedNames = true, ProcessDictionaryKeys = true
},
};

This takes advantage of the fact that, as shown in the reference source, CamelCasePropertyNamesContractResolver is basically just a subclass of DefaultContractResolver that uses a CamelCaseNamingStrategy with ProcessDictionaryKeys = true and OverrideSpecifiedNames = true.

Notes:

  • Naming strategies were introduces in Json.NET 9.0.1 so this answer does not apply to earlier versions.

  • You may want to cache the contract resolver statically for best performance.

  • By setting NamingStrategy.ProcessDictionaryKeys to true, the naming strategy will be applied to all dictionary keys.



Related Topics



Leave a reply



Submit