Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deserialize false as null (System.Text.Json)

I'm using an API which for some reason uses false where it should be using null. I can't figure out how to deserialize this properly. I tried to create a custom JsonConverter to solve this, but was unable to do so. I'd like to avoid using the dynamic type. How should I deserialize this?

This is the default response.

{
    "products": [
        {
            "id": 123456789,
            "supplier": {
                "id": 123456,
                "title": "abc"
            }
        }
    ]
}

Which I am deserializing as following.

public class Container
{
    public Product[] products { get; set; }
}

public class Product
{
    public ulong id { get; set; }
    public Supplier supplier { get; set; }
}

public class Supplier
{
    public ulong id { get; set; }
    public string title { get; set; }
}

JsonSerializer.Deserialize<Container>(json)

And this is the response when there is no supplier for the product.

{
    "products": [
        {
            "id": 123456789,
            "supplier": false
        }
    ]
}
like image 890
Pete Avatar asked Nov 07 '22 09:11

Pete


2 Answers

You can create your custom JsonConverter for the supplier property:

public class Product
{
    public ulong id { get; set; }

    [JsonConverter(typeof(SupplierConverter))]
    public Supplier supplier { get; set; }
}

public class SupplierConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
       if (reader.TokenType == JsonToken.Boolean)
       {
           if ((bool)reader.Value == false)
               return null;
       }

       return serializer.Deserialize(reader, objectType);
    }

    public override bool CanConvert(Type objectType)
    {
        return false;
    }
}

Update:

If you are using System.Text.Json, you can try the following custom converter:

public class SupplierConverter : JsonConverter<Supplier>
{
    public override Supplier Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.False)
            return null;

        if (options.GetConverter(typeof(JsonElement)) is JsonConverter<JsonElement> converter)
        {
            var json = converter.Read(ref reader, typeToConvert, options).GetRawText();

            return JsonSerializer.Deserialize<Supplier>(json);
        }

        throw new JsonException();
    }

    public override void Write(Utf8JsonWriter writer, Supplier value, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }
}
like image 154
tzrm Avatar answered Nov 11 '22 03:11

tzrm


When using System.Text.Json, you can implement and register your own JsonConverter<T> for the Supplier property.

There are two ways you can consider implementing your converter, depending on your need (whether you need to use the Supplier object in multiple places OR if you need to use different JsonSerializerOption settings or not).

  1. Create a JsonConverter<Supplier> and add the custom logic to handle false. Leave the rest to the Deserialize call. In your implementation, do not pass in options. Then, register this converter within the options. This is the simplest approach.
// Don't register this converter using an attribute on the Supplier class 
// since you are calling Deserialize on this type directly within the converter.
public class SupplierConverter : JsonConverter<Supplier>
{
    public override Supplier Read(
        ref Utf8JsonReader reader, 
        Type typeToConvert, 
        JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.False)
        {
            return null;
        }

        // Skip passing options here to avoid stackoverflow
        // This approach won't work if you have other options that need to be honored
        // when deserializing Supplier.
        return JsonSerializer.Deserialize<Supplier>(ref reader);
    }

    public override void Write(
        Utf8JsonWriter writer, 
        Supplier value, 
        JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }
}

// Register the converter within options as follows
// and pass the options the JsonSerializer.Deserialize call.
var options = new JsonSerializerOptions
{
    Converters = {new SupplierConverter()}
};
  1. Alternatively, create a JsonConverter<Supplier> and add the custom logic to handle "false" along with the deserialization of the Supplier object. In this case, you can register this converter either within the options, or use it as an attribute on the Supplier class itself. Follow this approach if you need to use custom options settings for deserializing the supplier for some reason (such as case insensitive matching of the property names).
public class SupplierConverter : JsonConverter<Supplier>
{
    public override Supplier Read(
        ref Utf8JsonReader reader, 
        Type typeToConvert, 
        JsonSerializerOptions options)
    {
        // Put whatever special case condition here. 
        // I added a null token check as well, just in case.
        if (reader.TokenType == JsonTokenType.False 
            || reader.TokenType == JsonTokenType.Null)
        {
            return null;
        }

        var output = new Supplier();

        // Potentially add other error handling for invalid JSON, if needed.
        while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
        {
            if (reader.TokenType == JsonTokenType.PropertyName)
            {
                if (reader.ValueTextEquals("id"))
                {
                    if (!reader.Read()) throw new JsonException();
                    output.id = reader.GetUInt64();
                }
                else if (reader.ValueTextEquals("title"))
                {
                    if (!reader.Read()) throw new JsonException();
                    output.title = reader.GetString();
                }
            }
        }

        if (reader.TokenType != JsonTokenType.EndObject)
        {
            throw new JsonException();
        }

        return output;
    }

    public override void Write(
        Utf8JsonWriter writer, 
        Supplier value, 
        JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }
}

// Register the converter within options as follows
// and pass the options the JsonSerializer.Deserialize call.
var options = new JsonSerializerOptions
{
    Converters = {new SupplierConverter()}
};

// OR
// Use annotate your Supplier class with
// a JsonConverterAttribute.

This doc will be useful to you when writing custom converters:

https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to

Here are the relevant API docs:

  • https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonconverter-1?view=netcore-3.1
  • https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonconverterattribute?view=netcore-3.1

Here's a working example (for both when the json contains a false supplier and when it contains the actual supplier JSON object in the payload): https://dotnetfiddle.net/XFbXB1

like image 33
ahsonkhan Avatar answered Nov 11 '22 04:11

ahsonkhan