using System.Collections;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Runtime.Serialization.Formatters;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Collections.ObjectModel;
using System.Linq.Expressions;
using System.Text.Json.Serialization;
using Debug = System.Console;
public interface IHasValue
public readonly struct Optional<T> : IHasValue
public bool HasValue { get; }
public object GetValue() => Value;
public static implicit operator Optional<T>(T value) => new Optional<T>(value);
public override string ToString() => this.HasValue ? (this.Value?.ToString() ?? "null") : "unspecified";
public class TypeWithOptionalsConverter<T> : JsonConverter<T> where T : class, new()
class TypeWithOptionalsConverterContractFactory : JsonObjectContractFactory<T>
protected override Expression CreateSetterCastExpression(Expression e, Type t)
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Optional<>))
return Expression.Convert(Expression.Convert(e, t.GetGenericArguments()[0]), t);
return base.CreateSetterCastExpression(e, t);
static readonly TypeWithOptionalsConverterContractFactory contractFactory = new TypeWithOptionalsConverterContractFactory();
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
var properties = contractFactory.GetProperties(typeToConvert);
if (reader.TokenType == JsonTokenType.Null)
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException();
if (reader.TokenType == JsonTokenType.EndObject)
if (reader.TokenType != JsonTokenType.PropertyName)
throw new JsonException();
string propertyName = reader.GetString();
if (!properties.TryGetValue(propertyName, out var property) || property.SetValue == null)
var type = property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Optional<>)
? property.PropertyType.GetGenericArguments()[0] : property.PropertyType;
var item = JsonSerializer.Deserialize(ref reader, type, options);
property.SetValue(value, item);
throw new JsonException();
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
writer.WriteStartObject();
foreach (var property in contractFactory.GetProperties(value.GetType()))
if (options.IgnoreReadOnlyProperties && property.Value.SetValue == null)
var item = property.Value.GetValue(value);
if (item is IHasValue hasValue)
writer.WritePropertyName(property.Key);
JsonSerializer.Serialize(writer, hasValue.GetValue(), options);
if (options.IgnoreNullValues && item == null)
writer.WritePropertyName(property.Key);
JsonSerializer.Serialize(writer, item, property.Value.PropertyType, options);
public class JsonPropertyContract<TBase>
internal JsonPropertyContract(PropertyInfo property, Func<Expression, Type, Expression> setterCastExpression)
this.GetValue = ExpressionExtensions.GetPropertyFunc<TBase>(property).Compile();
if (property.GetSetMethod() != null)
this.SetValue = ExpressionExtensions.SetPropertyFunc<TBase>(property, setterCastExpression).Compile();
this.PropertyType = property.PropertyType;
public Func<TBase, object> GetValue { get; }
public Action<TBase, object> SetValue { get; }
public Type PropertyType { get; }
public class JsonObjectContractFactory<TBase>
protected virtual Expression CreateSetterCastExpression(Expression e, Type t) => Expression.Convert(e, t);
ConcurrentDictionary<Type, ReadOnlyDictionary<string, JsonPropertyContract<TBase>>> Properties { get; } =
new ConcurrentDictionary<Type, ReadOnlyDictionary<string, JsonPropertyContract<TBase>>>();
ReadOnlyDictionary<string, JsonPropertyContract<TBase>> CreateProperties(Type type)
if (!typeof(TBase).IsAssignableFrom(type))
throw new ArgumentException();
.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy)
.Where(p => p.GetIndexParameters().Length == 0 && p.GetGetMethod() != null
&& !Attribute.IsDefined(p, typeof(System.Text.Json.Serialization.JsonIgnoreAttribute)))
.ToDictionary(p => p.GetCustomAttribute<System.Text.Json.Serialization.JsonPropertyNameAttribute>()?.Name ?? p.Name,
p => new JsonPropertyContract<TBase>(p, (e, t) => CreateSetterCastExpression(e, t)),
StringComparer.OrdinalIgnoreCase);
return dictionary.ToReadOnly();
public IReadOnlyDictionary<string, JsonPropertyContract<TBase>> GetProperties(Type type) => Properties.GetOrAdd(type, t => CreateProperties(t));
public static class DictionaryExtensions
public static ReadOnlyDictionary<TKey, TValue> ToReadOnly<TKey, TValue>(this IDictionary<TKey, TValue> dictionary) =>
new ReadOnlyDictionary<TKey, TValue>(dictionary ?? throw new ArgumentNullException());
public static class ExpressionExtensions
public static Expression<Func<T, object>> GetPropertyFunc<T>(PropertyInfo property)
var arg = Expression.Parameter(typeof(T), "x");
var getter = Expression.Property(arg, property);
var cast = Expression.Convert(getter, typeof(object));
return Expression.Lambda<Func<T, object>>(cast, arg);
public static Expression<Action<T, object>> SetPropertyFunc<T>(PropertyInfo property, Func<Expression, Type, Expression> setterCastExpression)
var arg1 = Expression.Parameter(typeof(T), "x");
var arg2 = Expression.Parameter(typeof(object), "y");
var cast = setterCastExpression(arg2, property.PropertyType);
var setter = Expression.Call(arg1, property.GetSetMethod(), cast);
return Expression.Lambda<Action<T, object>>(setter, arg1, arg2);
[JsonPropertyName("foo")]
public Optional<int?> Foo { get; set; }
[JsonPropertyName("bar")]
public Optional<int?> Bar { get; set; }
[JsonPropertyName("baz")]
public Optional<int?> Baz { get; set; }
[JsonPropertyName("nonNullable")]
public Optional<int> NonNullable { get; set; }
[JsonPropertyName("nonNullableSecond")]
public Optional<int> nonNullableSecond { get; set; }
public static void Test()
var options = new JsonSerializerOptions();
options.Converters.Add(new TypeWithOptionalsConverter<CustomType>());
Console.WriteLine(JsonSerializer.Serialize(new CustomType { Foo = 0, Bar = null }, options));
string json = @"[{""foo"":0,""bar"":null},{},{""foo"":0},{""nonNullable"":0},null]";
var parsed = JsonSerializer.Deserialize<List<CustomType>>(json, options);
string roundtrippedJson = JsonSerializer.Serialize(parsed, options);
Console.WriteLine("json: " + json);
Console.WriteLine("roundtrippedJson: " + roundtrippedJson);
Assert.AreEqual(json, roundtrippedJson);
public static void Main()
Console.WriteLine("Environment version: {0} ({1})", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , GetNetCoreVersion());
Console.WriteLine("System.Text.Json version: " + typeof(JsonSerializer).Assembly.FullName);
Console.WriteLine("Failed with unhandled exception: ");
public static string GetNetCoreVersion()
var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly;
var assemblyPath = assembly.CodeBase.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App");
if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2)
return assemblyPath[netCoreAppIndex + 1];