22

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]
Community
  • 1
  • 1
Martin
  • 2,956
  • 7
  • 30
  • 59
  • 3
    Clearly you want both to work when deserializing (and it may be possible to do using a custom JsonConverter or ContractResolver), but which one should be used while serializing? You wouldn't want both to be written out in that case, would you? – Brian Rogers Nov 05 '13 at 15:44
  • Indeed! I'd opt for FooBar but that's a detail. So which one, JsonConverter or ContractResolver and how? No attribute entirely would work as well or just one on the class. – Martin Nov 07 '13 at 07:02
  • Sorry for the delayed reply. I've added an answer showing how to accomplish this with a `JsonConverter` or, alternatively, by modifying the Json.Net source code. Hope this helps. – Brian Rogers Nov 10 '13 at 03:26

2 Answers2

31

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.

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • 1
    Love the answer. Having some inner resistance to changing the base code, since I can see how someone does this, says nothing to the team, and somewhere down the track, when someone expects one behavior and gets a different one, the chaos begins :) – Noctis Apr 14 '14 at 03:28
9

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);
VMAtm
  • 27,943
  • 17
  • 79
  • 125
Adrian Petrescu
  • 463
  • 5
  • 5
  • I must be missing something, but I can't figure it out. I created a .Net Fiddle here: https://dotnetfiddle.net/POERAH . Whenever I debug the code, the WritePropertyName override, isn't being hit. The Value method for CustomJsonReader is being hit, but throwing the given key was not present in dictionary error. – ajpetersen Jan 18 '16 at 22:29
  • I wasn't able to get this solution to work for me, but below is another alternative I ended up going with in case it's helpful for someone else. http://stackoverflow.com/questions/15915503/net-newtonsoft-json-deserialize-map-to-a-different-property-name – ajpetersen Jan 19 '16 at 13:59