Skip to content
KorsG edited this page Oct 5, 2018 · 4 revisions

When working with JSON-data, it is rather useful to be able to serialize/deserialize options into a nullable JSON-value.

For example, we would like (de)serialization to work as follows:

Serialization:
Some("abc") -> "abc"
None        -> null

Deserialization:
"abc"       -> Some("abc")
null        -> None

However, by default this will not work. If we use Newtonsoft.Json, we get something like the following:

var user = new User(name: "John Doe".Some(), email: Option.None<string>());
var json = JsonConvert.SerializeObject(user);

// Resulting json: { "user" { "HasValue": true }, "email": { "HasValue": false } }

... which is not terribly useful. However, using Newtonsoft.Json this can easily be remedied using a suitable JsonConverter, such as this:

using Newtonsoft.Json;
using Optional;
using System;
using System.Linq;
using System.Reflection;

namespace Optional.Json
{
    public class OptionConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            if (objectType == null) throw new ArgumentNullException(nameof(objectType));
            return objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(Option<>);
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (reader == null) throw new ArgumentNullException(nameof(reader));
            if (objectType == null) throw new ArgumentNullException(nameof(objectType));
            if (serializer == null) throw new ArgumentNullException(nameof(serializer));

            var innerType = objectType.GetGenericArguments()?.FirstOrDefault() ?? throw new InvalidOperationException("No inner type found.");
            var noneMethod = MakeStaticGenericMethodInfo(nameof(None), innerType);
            var someMethod = MakeStaticGenericMethodInfo(nameof(Some), innerType);

            if (reader.TokenType == JsonToken.Null)
            {
                return noneMethod.Invoke(null, new object[] { });
            }

            var innerValue = serializer.Deserialize(reader, innerType);

            if (innerValue == null)
            {
                return noneMethod.Invoke(null, new object[] { });
            }

            return someMethod.Invoke(noneMethod, new[] { innerValue });
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            if (writer == null) throw new ArgumentNullException(nameof(writer));
            if (serializer == null) throw new ArgumentNullException(nameof(serializer));

            if (value == null)
            {
                writer.WriteNull();
                return;
            }

            var innerType = value.GetType()?.GetGenericArguments()?.FirstOrDefault() ?? throw new InvalidOperationException("No inner type found.");
            var hasValueMethod = MakeStaticGenericMethodInfo(nameof(HasValue), innerType);
            var getValueMethod = MakeStaticGenericMethodInfo(nameof(GetValue), innerType);

            var hasValue = (bool)hasValueMethod.Invoke(null, new[] { value });

            if (!hasValue)
            {
                writer.WriteNull();
                return;
            }

            var innerValue = getValueMethod.Invoke(null, new[] { value });
            serializer.Serialize(writer, innerValue);
        }

        private MethodInfo MakeStaticGenericMethodInfo(string name, params Type[] typeArguments)
        {
            return GetType()
                ?.GetMethod(name, BindingFlags.NonPublic | BindingFlags.Static)
                ?.MakeGenericMethod(typeArguments)
                ?? throw new InvalidOperationException($"Could not make generic MethodInfo for method '{name}'.");
        }

        private static bool HasValue<T>(Option<T> option) => option.HasValue;
        private static T GetValue<T>(Option<T> option) => option.ValueOr(default(T));
        private static Option<T> None<T>() => Option.None<T>();
        private static Option<T> Some<T>(T value) => Option.Some(value);
    }
}

The above JsonConvert supports both serialization and deserialization.

A minor caveat is that the shown mapping is not invertible, when using nested options - that is serialization and deserialization are no longer each other's inverse operations. For example, Some(None) (e.g. of type Option<Option<string>>) would serialize to null, whereas null would still deserialize to None (the outer option is None). In practice, this is rarely a problem, though.

Clone this wiki locally