Pure.Krome
Pure.Krome

Reputation: 86957

JSON.NET - custom enum handling at deserialization

We have a JSON that we can deserialize into a custom domain model, no problems at all. It includes a property which is a custom enum:

public enum UserType
{
    President,
    Chump
}

We've now changed our enum class but still need to accept and deserialize the previous values of any JSON's that arrive. It's like we now have two versions of our JSON

public enum UserType
{
    President,
    Vice-President,
    Citizen  // Chump maps to Citizen, now.
}

and in the json itself..

"userType": "chump"; // needs to map to Citizen

I'm not sure how to do this.

Is this using JsonConverter ?

Also, this is our custom settings we use for all our serialization and deserializtion. NOTE: we serialize any enum to it's string description/value, not it's int value.

internal static JsonSerializerSettings JsonSerializerSettings => new JsonSerializerSettings
{
    Converters = new JsonConverter[]
    {
        new StringEnumConverter()
    },
    Formatting = Formatting.Indented
};

Upvotes: 2

Views: 3919

Answers (2)

Martin Zikmund
Martin Zikmund

Reputation: 39082

You can implement a custom Converter deriving from StringEnumConverter:

public class UserTypeEnumConverter : StringEnumConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(UserType);
    }

    public override object ReadJson(JsonReader reader, 
                                    Type objectType, 
                                    object existingValue, 
                                    JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
        {
            var isNullable = (Nullable.GetUnderlyingType(objectType) != null);
            if (!isNullable)
            {
                throw new JsonSerializationException();
            }
            return null;
        }

        var token = JToken.Load(reader);
        var value = token.ToString();
        if (string.Equals(value, "chump", StringComparison.OrdinalIgnoreCase))
        {
            return UserType.Citizen;
        }
        else
        {
            return base.ReadJson(reader, objectType, existingValue, serializer);
        }               
    }
}

The converter handles the case when the underlying type is nullable as well and is invoked only when the type of property declared on the deserialized class is UserType. It then checks if the value being parsed is the "deprecated" one and if not, it delegates the reading to the base StringEnumConverter.

Upvotes: 1

Omar Muscatello
Omar Muscatello

Reputation: 1301

In your Enum just add the EnumMember attribute which specify the value for serialization/deserialization process.

public enum UserType
{
    President,
    VicePresident,

    [EnumMember(Value = "chump")]
    Citizen  // Chump maps to Citizen, now.
}

The property userType will be Citizen when, in your json, the userType property is equal to "Chump" or "Citizen".

Remember to add the System.Runtime.Serialization reference to your project.

Edit

I noticed that the check of the Value property of EnumMember attribute is case-sensitive. So you can't use "Chump" if in your json you have "chump". To solve this problem you can use a custom StringEnumConverter.

public class UserTypeEnumConverter : StringEnumConverter
{
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var userTypeType = typeof(UserType);
        if (objectType == userTypeType)
        {
            var value = reader.Value.ToString().ToLower();

            foreach (var item in Enum.GetNames(userTypeType))
            {
                var memberValue = GetEnumMemberValue(userTypeType.GetMember(item)[0]);
                if (memberValue != null && memberValue.ToLower() == value)
                {
                    return Enum.Parse(userTypeType, item);
                }
            }
        }

        return base.ReadJson(reader, objectType, existingValue, serializer);
    }
}

private static string GetEnumMemberValue(MemberInfo memberInfo)
{
    var attributes = memberInfo.GetCustomAttributes(typeof(EnumMemberAttribute), inherit: false);

    if (attributes.Length == 0) return null;

    return ((EnumMemberAttribute)attributes[0]).Value;
}

In the above code, I check only the EnumMember attribute because the UserType's members case-insensitive check is already done by the default StringEnumConvert.

Note that this converter will work only for your UserType enum beacuse of the check:

var userTypeType = typeof(UserType);
if (objectType == userTypeType)
{

Replace the JsonSerializerSettings initialization with:

internal static JsonSerializerSettings JsonSerializerSettings => new JsonSerializerSettings
{
    Converters = new JsonConverter[]
    {
        new UserTypeEnumConverter()
    },
    Formatting = Formatting.Indented
};

I assumed that Vice-President enumerator is VicePresident in UserType.

Upvotes: 7

Related Questions