using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
public interface IHasValue
[JsonConverter(typeof(OptionalConverter))]
public readonly struct Optional<T> : IHasValue
public Optional(T value) => (this.HasValue, this.Value) = (true, value);
public bool HasValue { get; }
object? IHasValue.GetValue() => Value;
public override string ToString() => this.HasValue ? (this.Value?.ToString() ?? "null") : "unspecified";
public static implicit operator Optional<T>(T value) => new Optional<T>(value);
public static implicit operator T(Optional<T> value) => value.Value;
class OptionalConverter : JsonConverter
static Type? GetValueType(Type objectType) =>
objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(Optional<>) ? objectType.GetGenericArguments()[0] : null;
public override bool CanConvert(Type objectType) => GetValueType(objectType) != null;
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
var valueType = GetValueType(objectType) ?? throw new ArgumentException(objectType.ToString());
var value = serializer.Deserialize(reader, valueType);
return Activator.CreateInstance(objectType, value);
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
=> serializer.Serialize(writer, ((IHasValue?)value)?.GetValue());
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public Optional<int> A { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public Optional<int?> B { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public Optional<string> SomeString { get; set; }
public record Tmp2(Optional<int> A, Optional<int?> B);
public class OptionalResolver : DefaultContractResolver
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
var property = base.CreateProperty(member, memberSerialization);
if (property.ValueProvider != null && property.Readable && (typeof(IHasValue).IsAssignableFrom(property.PropertyType) || typeof(IHasValue).IsAssignableTo(property.PropertyType)))
var old = property.ShouldSerialize;
Predicate<object> shouldSerialize = (o) => property.ValueProvider.GetValue(o) is IHasValue v ? v.HasValue : true;
property.ShouldSerialize = (old == null ? shouldSerialize : (o) => old(o) && shouldSerialize(o));
public static void Test()
Assert.That(!JsonConvert.DeserializeObject<Tmp2>("{}")!.A.HasValue);
Assert.That(JsonConvert.SerializeObject(new Tmp2 { B = 32 }) == "{\"B\":32}");
IContractResolver resolver = new OptionalResolver
NamingStrategy = new CamelCaseNamingStrategy(),
var settings = new JsonSerializerSettings { ContractResolver = resolver };
var inJson = "{\"b\" : null}";
var tmp = JsonConvert.DeserializeObject<Tmp2>(inJson, settings);
Console.WriteLine("Dumping result of deserializing JSON {0}", inJson);
var json2 = JsonConvert.SerializeObject(tmp, settings);
Console.WriteLine(json2);
Assert.AreEqual("{}", JsonConvert.SerializeObject(JsonConvert.DeserializeObject<Tmp2>("{}"), settings));
TestRoundTrip<Tmp2>("{}", settings);
TestRoundTrip<Tmp2>("{\"b\":null}", settings);
TestRoundTrip<Tmp2>("{\"b\":32}", settings);
TestRoundTrip<Tmp2>("{\"a\":32}", settings);
TestRoundTrip<Tmp2>("{\"someString\":\"foo\"}", settings);
TestRoundTrip<V2.Tmp2>("{}", settings);
TestRoundTrip<V2.Tmp2>("{\"b\":null}", settings);
TestRoundTrip<V2.Tmp2>("{\"b\":32}", settings);
TestRoundTrip<V2.Tmp2>("{\"a\":32}", settings);
static void TestRoundTrip<Tmp2>(string json, JsonSerializerSettings settings)
var tmp = JsonConvert.DeserializeObject<Tmp2>(json, settings);
var newJson = JsonConvert.SerializeObject(tmp, settings);
Assert.That(JToken.DeepEquals(JToken.Parse(json), JToken.Parse(newJson)));
public static void Main()
Console.WriteLine("Environment version: {0} ({1}), {2}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , Environment.Version, Environment.OSVersion);
Console.WriteLine("{0} version: {1}", typeof(JsonSerializer).Namespace, typeof(JsonSerializer).Assembly.FullName);
Console.WriteLine("Failed with unhandled exception: ");