Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Json.Net Rename properties during serialization [duplicate]

Tags:

c#

json.net

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]
like image 709
Martin Avatar asked Nov 23 '22 10:11

Martin


2 Answers

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.

like image 122
Brian Rogers Avatar answered Dec 10 '22 19:12

Brian Rogers


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);
like image 26
Adrian Petrescu Avatar answered Dec 10 '22 18:12

Adrian Petrescu