Serializing and Deserializing Enumerations with Json.NET

This article shows you how to serialize and deserialize enums with Json.NET.

If you write a client for a RESTful API, then you often have to deal with error codes. In a typed language like C# you should never throw strings at the user of your API client (for your users sanity), so error codes need to be converted from their string representation into an enumeration of your library.

Some time ago I have written [FcmSharp], which is a client for the Google Cloud Messaging (GCM) API. This example is based on the implementation at:

The error codes for the Google Cloud Messaging API are described in the HTTP Server Reference: Error Codes.

ErrorCode Enum

First of all we are going to define the enum, that matches the error codes in the documentation (HTTP Server Reference: Error Codes).

// Copyright (c) Philipp Wagner. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace GcmSharp.Responses
{
    public enum ErrorCode
    {
        InvalidRegistration,
        NotRegistered,
        MessageTooBig,
        MissingRegistration,
        Unavailable,
        MismatchSenderId,
        InvalidDataKey,
        InvalidTtl,
        InternalServerError,
        InvalidPackageName,
        DeviceMessageRateExceeded,
        TopicsMessageRateExceeded
    }
}

Using a StringEnumConverter

Entity

Json.NET comes with the StringEnumConverter to convert between an enum and the JSON string representation. The property of the ErrorCode enum simply needs to be attributed as a JsonConverter of type StringEnumConverter in order to be serialized and deserialized.

public class SampleEntity
{
    [JsonProperty("error")]
    [JsonConverter(typeof(StringEnumConverter))]
    public ErrorCode Error { get; set; }
}

Unit Test

And then we can write a Unit Test to verify, that the entity is serialized and deserialized correctly.

// Copyright (c) Philipp Wagner. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using GcmSharp.Responses;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using NUnit.Framework;
using System.Collections.Generic;

namespace GcmSharp.Test.Responses.Converter
{
    [TestFixture]
    public class StringEnumConverterTest
    {
        public class SampleEntity
        {
            [JsonProperty("error")]
            [JsonConverter(typeof(StringEnumConverter))]
            public ErrorCode Error { get; set; }
        }

        [Test]
        public void DeserializeErrorCodeTest()
        {
            Dictionary<string, ErrorCode> expectations = GetErrorCodeMapping();

            foreach (var kv in expectations)
            {
                var jsonString = string.Format("{{ \"error\" : \"{0}\" }}", kv.Key);
                var deserializedObject = JsonConvert.DeserializeObject<SampleEntity>(jsonString);
                Assert.AreEqual(kv.Value, deserializedObject.Error);
            }
        }

        [Test]
        public void SerializeErrorCodeTest()
        {
            Dictionary<string, ErrorCode> expectations = GetErrorCodeMapping();

            foreach (var kv in expectations)
            {
                var obj = new SampleEntity { Error = kv.Value };

                var expectedJsonString = string.Format("{{\"error\":\"{0}\"}}", kv.Key);
                var actualJsonString = JsonConvert.SerializeObject(obj);

                Assert.AreEqual(expectedJsonString, actualJsonString);
            }
        }

        private Dictionary<string, ErrorCode> GetErrorCodeMapping()
        {
            return new Dictionary<string, ErrorCode>()
            {
                { "MissingRegistration", ErrorCode.MissingRegistration},
                { "InvalidRegistration", ErrorCode.InvalidRegistration},
                { "NotRegistered", ErrorCode.NotRegistered},
                { "InvalidPackageName", ErrorCode.InvalidPackageName},
                { "MismatchSenderId", ErrorCode.MismatchSenderId},
                { "MessageTooBig", ErrorCode.MessageTooBig},
                { "InvalidDataKey", ErrorCode.InvalidDataKey},
                { "InvalidTtl", ErrorCode.InvalidTtl},
                { "Unavailable", ErrorCode.Unavailable},
                { "InternalServerError", ErrorCode.InternalServerError},
                { "DeviceMessageRateExceeded",ErrorCode.DeviceMessageRateExceeded },
                { "TopicsMessageRateExceeded", ErrorCode.TopicsMessageRateExceeded},
            };
        }
    }
}

Using a Custom JsonConverter

ErrorCodeConverter

It may be possible, that the enum values and the JSON string representation of the enum do not match. If that's the case, you can't use a StringEnumConverter, but have to implement your own JsonConverter, to do the conversion.

// Copyright (c) Philipp Wagner. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Newtonsoft.Json;
using System;

namespace GcmSharp.Responses.Converter
{
    public class ErrorCodeConverter : JsonConverter
    {
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            ErrorCode messageTransportResponseStatus = (ErrorCode)value;

            switch (messageTransportResponseStatus)
            {
                case ErrorCode.MissingRegistration:
                    writer.WriteValue("MissingRegistration");
                    break;
                case ErrorCode.InvalidRegistration:
                    writer.WriteValue("InvalidRegistration");
                    break;
                case ErrorCode.NotRegistered:
                    writer.WriteValue("NotRegistered");
                    break;
                case ErrorCode.InvalidPackageName:
                    writer.WriteValue("InvalidPackageName");
                    break;
                case ErrorCode.MismatchSenderId:
                    writer.WriteValue("MismatchSenderId");
                    break;
                case ErrorCode.MessageTooBig:
                    writer.WriteValue("MessageTooBig");
                    break;
                case ErrorCode.InvalidDataKey:
                    writer.WriteValue("InvalidDataKey");
                    break;
                case ErrorCode.InvalidTtl:
                    writer.WriteValue("InvalidTtl");
                    break;
                case ErrorCode.Unavailable:
                    writer.WriteValue("Unavailable");
                    break;
                case ErrorCode.InternalServerError:
                    writer.WriteValue("InternalServerError");
                    break;
                case ErrorCode.DeviceMessageRateExceeded:
                    writer.WriteValue("DeviceMessageRateExceeded");
                    break;
                case ErrorCode.TopicsMessageRateExceeded:
                    writer.WriteValue("TopicsMessageRateExceeded");
                    break;
            }
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var enumString = (string)reader.Value;

            return Enum.Parse(typeof(ErrorCode), enumString, true);
        }

        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(string);
        }
    }
}

Using the JsonConverter for your Object

Similar to the StringEnumConverter you have to attribute the ErrorCode property with a JsonConverter attribute.

// Copyright (c) Philipp Wagner. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using GcmSharp.Responses;
using GcmSharp.Responses.Converter;
using Newtonsoft.Json;

namespace GcmSharp.Test.Responses.Converter
{
    public class SampleEntity
    {
        [JsonProperty("error")]
        [JsonConverter(typeof(ErrorCodeConverter))]
        public ErrorCode Error { get; set; }
    }
}

Unit Test

Finally we can write a Unit Test to ensure the serialization and deserialization works as expected.

// Copyright (c) Philipp Wagner. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using GcmSharp.Responses;
using Newtonsoft.Json;
using NUnit.Framework;
using System.Collections.Generic;

namespace GcmSharp.Test.Responses.Converter
{
    [TestFixture]
    public class ErrorCodeConverterTest
    {
        [Test]
        public void DeserializeErrorCodeTest()
        {
            Dictionary<string, ErrorCode> expectations = GetErrorCodeMapping();

            foreach (var kv in expectations)
            {
                var jsonString = string.Format("{{ \"error\" : \"{0}\" }}", kv.Key);
                var deserializedObject = JsonConvert.DeserializeObject<SampleEntity>(jsonString);
                Assert.AreEqual(kv.Value, deserializedObject.Error);
            }
        }

        [Test]
        public void SerializeErrorCodeTest()
        {
            Dictionary<string, ErrorCode> expectations = GetErrorCodeMapping();

            foreach (var kv in expectations)
            {
                var obj = new SampleEntity { Error = kv.Value };

                var expectedJsonString = string.Format("{{\"error\":\"{0}\"}}", kv.Key);
                var actualJsonString = JsonConvert.SerializeObject(obj);

                Assert.AreEqual(expectedJsonString, actualJsonString);
            }
        }

        private Dictionary<string, ErrorCode> GetErrorCodeMapping()
        {
            return new Dictionary<string, ErrorCode>()
            {
                { "MissingRegistration", ErrorCode.MissingRegistration},
                { "InvalidRegistration", ErrorCode.InvalidRegistration},
                { "NotRegistered", ErrorCode.NotRegistered},
                { "InvalidPackageName", ErrorCode.InvalidPackageName},
                { "MismatchSenderId", ErrorCode.MismatchSenderId},
                { "MessageTooBig", ErrorCode.MessageTooBig},
                { "InvalidDataKey", ErrorCode.InvalidDataKey},
                { "InvalidTtl", ErrorCode.InvalidTtl},
                { "Unavailable", ErrorCode.Unavailable},
                { "InternalServerError", ErrorCode.InternalServerError},
                { "DeviceMessageRateExceeded",ErrorCode.DeviceMessageRateExceeded },
                { "TopicsMessageRateExceeded", ErrorCode.TopicsMessageRateExceeded},
            };
        }
    }
}