The simplest solution would be to return null!
:
#nullable enable
internal class JsonFalseOrObjectConverter<T> : JsonConverter<T> where T : class
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.False)
{
return null!;
}
else
{
return JsonSerializer.Deserialize<T>(ref reader);
}
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options){}
}
Reference classes are nullable so the compiler just uses T
when T?
is encountered.
A better option would be to create something similar to F#'s Option type, that contains Some value if a value is set, None if the value is false. By making that Option
a struct, we get a default None
value even when the property is missing or null :
readonly struct Option<T>
{
public readonly T Value {get;}
public readonly bool IsSome {get;}
public readonly bool IsNone =>!IsSome;
public Option(T value)=>(Value,IsSome)=(value,true);
public void Deconstruct(out T value)=>(value)=(Value);
}
//Convenience methods, similar to F#'s Option module
static class Option
{
public static Option<T> Some<T>(T value)=>new Option<T>(value);
public static Option<T> None<T>()=>default;
}
The deserializer can return None()
or default
if false is encountered:
internal class JsonFalseOrObjectConverter<T> : JsonConverter<Option<T>> where T : class
{
public override Option<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.False)
{
return Option.None<T>(); // or default
}
else
{
return Option.Some(JsonSerializer.Deserialize<T>(ref reader));
}
}
public override void Write(Utf8JsonWriter writer, Option<T> value, JsonSerializerOptions options)
{
switch (value)
{
case Option<T> (_ ,false) :
JsonSerializer.Serialize(writer,false,options);
break;
case Option<T> (var v,true) :
JsonSerializer.Serialize(writer,v,options);
break;
}
}
}
The Write
method shows how Option<T>
can be handled using pattern matching.
Using this serializer, the following classes :
class Category
{
public string Name{get;set;}
}
class Product
{
public string Name{get;set;}
public Option<Category> Category {get;set;}
}
Can be serialized with false
generated for missing categories :
var serializerOptions = new JsonSerializerOptions
{
Converters = { new JsonFalseOrObjectConverter<Category>() }
};
var product1=new Product{Name="A"};
var json=JsonSerializer.Serialize(product1,serializerOptions);
This returns :
{"Name":"A","Category":false}
Deserializing this string returns a Product
whose Category
is an Option<Category>
without a value :
var product2=JsonSerializer.Deserialize<Product>(json,serializerOptions);
Debug.Assert(product2.Category.IsNone);
Pattern matching expressions can be used to extract and use the Category's properties if it has a value, eg :
string category=product2.Category switch { Option<Category> (_ ,false) =>"No Category",
Option<Category> (var v,true) => v.Name};
Or
if(product2.Category is Option<Category>(var cat,true))
{
Console.WriteLine(cat.Name);
}
false
is a very specific JSON value too, notnull
or a missing value. If you can't change the API, consider returning a newT
"empty" instance instead of null. – Panagiotis KanavosOption <T>
type like the one described in this question or this issue, combined with pattern matching and returnNone
when the attribute isfalse
– Panagiotis Kanavos