1
votes

I'm using an API that returns JSON where one of its values can be either false or an object. To handle that, I've created a custom JsonConverter<T>.

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);
        }
    }
}

The problem is that I get the following compiler error:

Possible null reference return.

I could set the returned type to be a T? but then I would get the error:

Nullability of reference types in return type doesn't match overridden member.

How can I fix that?

2
Reference types are inherently nullable. Only value types (wich are usually implemented as structs) need to be given it explicitly. - Christopher
The error message tells you taht you are not allowed to return null from this function, as the function you are overriding does not allow null return values. I think we will need to see the function you are overriding here. - Christopher
I should have specified that I have enabled the nullable reference types from C# 8. The method I'm overriding is docs.microsoft.com/en-us/dotnet/api/… - Greg
You should rethink what you're doing. No matter the converter, an API that returns arbitrary values for the same attribute will confuse every client. A false is a very specific JSON value too, not null or a missing value. If you can't change the API, consider returning a new T"empty" instance instead of null. - Panagiotis Kanavos
You could use an Option <T> type like the one described in this question or this issue, combined with pattern matching and return None when the attribute is false - Panagiotis Kanavos

2 Answers

2
votes

You have stated that the generic type is a (non-nullable) T yet you are returning null. That clearly cannot be valid.

You would need to make your converter implement JsonConverter<T?> or use the null forgiving operator if you just don't care.

internal class JsonFalseOrObjectConverter<T> : JsonConverter<T?> where T : class
{
    public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        ...
    }

    public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
    {
        ...
    }
}

or

public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    if (reader.TokenType == JsonTokenType.False)
    {
        return null!;
    }
    ...
}
1
votes

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);
}