-
Notifications
You must be signed in to change notification settings - Fork 73
Working with JSON
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.