TheRoadrunner
TheRoadrunner

Reputation: 1499

NewtonSoft JsonConverter - Rename property to value of other property

I have this class:

public class ValueInteger
{
    [JsonIgnore]
    public string ValueName { get; set; }

    public int Value { get; set; }

    [JsonProperty("timestamp")]
    public UInt64 TimeStamp { get; set; }

}

Given an instance of:

var valueInt = new ValueInteger 
    { 
        ValueName = "mycounter",
        Value = 7,
        TimeStamp = 1010101010
    }

It should serialize to:

{ mycounter: 7, timestamp = 1010101010 }

It would be cool if one could declare the Value property as

    [JsonRedirect(titlePropertyName: nameof(ValueName))]
    public int Value { get; set; }

I probably have to implement my own ContractResolver, and have studiet this post: https://stackoverflow.com/a/47872645/304820 but it depends on the IValueProvider, and AFAIK there is no INameProvider to use for renaming.

Usually renaming is done per class, not per instance.

Upvotes: 3

Views: 11127

Answers (1)

Anders Carstensen
Anders Carstensen

Reputation: 4194

My approach to this would be to write my own Converter. The converter simply serializes in the same fashion as a normal converter, but whenever it comes across a special attribute on a property, it should rename that property in the output.

So, serializing a C# object would go like this:

  1. First convert the C# object into a JSON object (i.e. JTokens structure).
  2. Run through the properties in the C# object and find the ones that need to be renamed...
  3. For each of those properties, determine what their current name is and what their new name should be.
  4. Perform the renaming on the JSON object.
  5. Finally serialize the JSON object into a string.

I have made a simple implementation of this. The usage looks like this:

class Program
{
    static void Main(string[] args)
    {
        var valueInt = new ValueInteger
        {
            ValueName = "mycounter",
            Value = 7,
            TimeStamp = 1010101010
        };

        var settings = new JsonSerializerSettings { Converters = new JsonConverter[] { new DynamicNameConverter() } };
        var result = JsonConvert.SerializeObject(valueInt, settings);
        Console.WriteLine(result);
        Console.Read();
    }
}


public class ValueInteger
{
    [JsonIgnore]
    public string ValueName { get; set; }

    [JsonDynamicName(nameof(ValueName))]
    public int Value { get; set; }

    [JsonProperty("timestamp")]
    public UInt64 TimeStamp { get; set; }
}

And the helper classes:

class DynamicNameConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        // Only use this converter for classes that contain properties with an JsonDynamicNameAttribute.
        return objectType.IsClass && objectType.GetProperties().Any(prop => prop.CustomAttributes.Any(attr => attr.AttributeType == typeof(JsonDynamicNameAttribute)));
    }

    public override bool CanRead => false;
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // We do not support deserialization.
        throw new NotImplementedException();
    }

    public override bool CanWrite => true;
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var token = JToken.FromObject(value);
        if (token.Type != JTokenType.Object)
        {
            // We should never reach this point because CanConvert() only allows objects with JsonPropertyDynamicNameAttribute to pass through.
            throw new Exception("Token to be serialized was unexpectedly not an object.");
        }

        JObject o = (JObject)token;
        var propertiesWithDynamicNameAttribute = value.GetType().GetProperties().Where(
            prop => prop.CustomAttributes.Any(attr => attr.AttributeType == typeof(JsonDynamicNameAttribute))
        );

        foreach (var property in propertiesWithDynamicNameAttribute)
        {
            var dynamicAttributeData = property.CustomAttributes.FirstOrDefault(attr => attr.AttributeType == typeof(JsonDynamicNameAttribute));

            // Determine what we should rename the property from and to.
            var currentName = property.Name;
            var propertyNameContainingNewName = (string)dynamicAttributeData.ConstructorArguments[0].Value;
            var newName = (string)value.GetType().GetProperty(propertyNameContainingNewName).GetValue(value);

            // Perform the renaming in the JSON object.
            var currentJsonPropertyValue = o[currentName];
            var newJsonProperty = new JProperty(newName, currentJsonPropertyValue);
            currentJsonPropertyValue.Parent.Replace(newJsonProperty);
        }

        token.WriteTo(writer);
    }
}

[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
class JsonDynamicNameAttribute : Attribute
{
    public string ObjectPropertyName { get; }
    public JsonDynamicNameAttribute(string objectPropertyName)
    {
        ObjectPropertyName = objectPropertyName;
    }
}

Please be aware that a lot of error handling could be put into DynamicNameConverter but I have left it out to make it easier to read and understand.

Upvotes: 5

Related Questions