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