In reference to this question:
How can I change property names when serializing with Json.net?
Sure, great, but can I have the cake and eat it?
What I'm looking for is an eye pleasing way have an alternate name for a property in such a way that the string may contain either.
Something like:
[BetterJsonProperty(PropertyName = "foo_bar")]
public string FooBar { get; set; }
Both
{
"FooBar": "yup"
}
and
{
"foo_bar":"uhuh"
}
would deserialize as expected.
As solution with no attribute would work or an attribute on the class like:
[AllowCStylePropertyNameAlternatives]
One way to accomplish this is to create a custom JsonConverter
. The idea is to have the converter enumerate the JSON property names for objects we are interested in, strip the non-alphanumeric characters from the names and then try to match them up with the actual object properties via reflection. Here is how it might look in code:
public class LaxPropertyNameMatchingConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType.IsClass;
}
public override bool CanWrite
{
get { return false; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
object instance = objectType.GetConstructor(Type.EmptyTypes).Invoke(null);
PropertyInfo[] props = objectType.GetProperties();
JObject jo = JObject.Load(reader);
foreach (JProperty jp in jo.Properties())
{
string name = Regex.Replace(jp.Name, "[^A-Za-z0-9]+", "");
PropertyInfo prop = props.FirstOrDefault(pi =>
pi.CanWrite && string.Equals(pi.Name, name, StringComparison.OrdinalIgnoreCase));
if (prop != null)
prop.SetValue(instance, jp.Value.ToObject(prop.PropertyType, serializer));
}
return instance;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
To use the custom converter with a particular class, you can decorate that class with a [JsonConverter]
attribute like this:
[JsonConverter(typeof(LaxPropertyNameMatchingConverter))]
public class MyClass
{
public string MyProperty { get; set; }
public string MyOtherProperty { get; set; }
}
Here is a simple demo of the converter in action:
class Program
{
static void Main(string[] args)
{
string json = @"
[
{
""my property"" : ""foo"",
""my-other-property"" : ""bar"",
},
{
""(myProperty)"" : ""baz"",
""myOtherProperty"" : ""quux""
},
{
""MyProperty"" : ""fizz"",
""MY_OTHER_PROPERTY"" : ""bang""
}
]";
List<MyClass> list = JsonConvert.DeserializeObject<List<MyClass>>(json);
foreach (MyClass mc in list)
{
Console.WriteLine(mc.MyProperty);
Console.WriteLine(mc.MyOtherProperty);
}
}
}
Output:
foo
bar
baz
quux
fizz
bang
While this solution should do the job in most cases, there is an even simpler solution if you are OK with the idea of changing the Json.Net source code directly. It turns out you can accomplish the same thing by adding just one line of code to the Newtonsoft.Json.Serialization.JsonPropertyCollection
class. In this class, there is a method called GetClosestMatchProperty()
which looks like this:
public JsonProperty GetClosestMatchProperty(string propertyName)
{
JsonProperty property = GetProperty(propertyName, StringComparison.Ordinal);
if (property == null)
property = GetProperty(propertyName, StringComparison.OrdinalIgnoreCase);
return property;
}
At the point where this method is called by the deserializer, the JsonPropertyCollection
contains all the properties from the class being deserialized, and the propertyName
parameter contains the name of the JSON property name being matched. As you can see, the method first tries an exact name match, then it tries a case-insensitive match. So we already have a many-to-one mapping being done between the JSON and class property names.
If you modify this method to strip out all non-alphanumeric characters from the property name prior to matching it, then you can get the behavior you desire, without any special converters or attributes needed. Here is the modified code:
public JsonProperty GetClosestMatchProperty(string propertyName)
{
propertyName = Regex.Replace(propertyName, "[^A-Za-z0-9]+", "");
JsonProperty property = GetProperty(propertyName, StringComparison.Ordinal);
if (property == null)
property = GetProperty(propertyName, StringComparison.OrdinalIgnoreCase);
return property;
}
Of course, modifying the source code has its problems as well, but I figured it was worth a mention.
Another way of accomplishing this is intercepting the serialization/deserialization process early, by doing some overrides the JsonReader
and JsonWriter
public class CustomJsonWriter : JsonTextWriter
{
private readonly Dictionary<string, string> _backwardMappings;
public CustomJsonWriter(TextWriter writer, Dictionary<string, string> backwardMappings)
: base(writer)
{
_backwardMappings = backwardMappings;
}
public override void WritePropertyName(string name)
{
base.WritePropertyName(_backwardMappings[name]);
}
}
public class CustomJsonReader : JsonTextReader
{
private readonly Dictionary<string, string> _forwardMappings;
public CustomJsonReader(TextReader reader, Dictionary<string, string> forwardMappings )
: base(reader)
{
_forwardMappings = forwardMappings;
}
public override object Value
{
get
{
if (TokenType != JsonToken.PropertyName)
return base.Value;
return _forwardMappings[base.Value.ToString()];
}
}
}
After doing this, you can serialize by doing
var mappings = new Dictionary<string, string>
{
{"Property1", "Equivalent1"},
{"Property2", "Equivalent2"},
};
var builder = new StringBuilder();
JsonSerializer.Create().Serialize(new CustomJsonWriter(new StringWriter(builder), mappings), your_object);
and deserialize by doing
var mappings = new Dictionary<string, string>
{
{"Equivalent1", "Property1"},
{"Equivalent2", "Property2"},
};
var txtReader = new CustomJsonReader(new StringReader(jsonString), mappings);
var your_object = JsonSerializer.Create().Deserialize<Your_Type>(txtReader);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With