Home > database >  C# JsonSerializer.Deserialize fails if property has null value despite JsonIgnoreCondition.WhenWriti
C# JsonSerializer.Deserialize fails if property has null value despite JsonIgnoreCondition.WhenWriti

Time:12-20

From an external webservice i receive either

// jsonWithConfig 
// property config is an object {}
{"config":{"c1":"All","c2":"is peachy"},"message":"We found a config object"} 

// jsonNoConfig
// property config is string with the value null
{"config":"null","message":"Config is null"} 

I want to deserialize the json into these types


public class WebResponse
{     
   [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]   
   public Config Config { get; set; }  
   public string Message { get; set; } 

   // i also tried dynamic but 
   //    a) this is not what i want
   //    b) it resulted in RuntimeBinderException
   // public dynamic Config { get; set; } 
}

public class Config
{
    public string C1 { get; set; }
    public string C2 { get; set; }
}

What have i tried?

From Linqpad demo

CodePudding user response:

As explained in comments by MySkullCaveIsADarkPlace, your problem is that the JSON value "null"

"config":"null"

Is not null. It is a non-null string value containing the characters null. A null value looks like:

"config":null // Notice there are no quotes around the text

For confirmation, see the original JSON proposal.

If you cannot fix the JSON to represent null values properly, you will need to write a custom JsonConverter that checks for a "null" text value and returns null if present. If not present, the converter should proceed with default deserialization.

The question How to use default serialization in a custom System.Text.Json JsonConverter? has this answer which provides a DefaultConverterFactory<T>. Grab it and subclass it as follows:

NullTextValueForNullObjectConverter

public sealed class NullTextValueForNullObjectConverter<T> :
 DefaultConverterFactory<T> where T : class
{
   const string NullValue = "null";
        
   protected override T Read(ref Utf8JsonReader reader, Type typeToConvert, 
                 JsonSerializerOptions modifiedOptions) 
   {
      if (reader.TokenType == JsonTokenType.String)
      {
        var s = reader.GetString();
        // Or use StringComparison.Ordinal if you are sure the 
        // text "null" will always be lowercase
        if (string.Equals(s, NullValue, StringComparison.OrdinalIgnoreCase)) 
                    return null;
      }
      return (T)JsonSerializer.Deserialize(ref reader, typeToConvert);
    }
}

DefaultConverterFactory

public abstract class DefaultConverterFactory<T> : JsonConverterFactory
{
    class DefaultConverter : JsonConverter<T>
    {
       readonly JsonSerializerOptions modifiedOptions;
       readonly DefaultConverterFactory<T> factory;

       public DefaultConverter(JsonSerializerOptions options, DefaultConverterFactory<T> factory)
       {
           this.factory = factory;
           this.modifiedOptions = options.CopyAndRemoveConverter(factory.GetType());
       }

       public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => factory.Write(writer, value, modifiedOptions);

       public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 
            => factory.Read(ref reader, typeToConvert, modifiedOptions);
    }

    protected virtual T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions modifiedOptions)
            => (T)JsonSerializer.Deserialize(ref reader, typeToConvert, modifiedOptions);

    protected virtual void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions modifiedOptions) 
            => JsonSerializer.Serialize(writer, value, modifiedOptions);

    public override bool CanConvert(Type typeToConvert) 
            => typeof(T) == typeToConvert;

    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) 
            => new DefaultConverter(options, this);
}

JsonSerializerExtensions

public static class JsonSerializerExtensions
{
    public static JsonSerializerOptions CopyAndRemoveConverter(this JsonSerializerOptions options, Type converterType)
    {
        var copy = new JsonSerializerOptions(options);
        for (var i = copy.Converters.Count - 1; i >= 0; i--)
            if (copy.Converters[i].GetType() == converterType)
                copy.Converters.RemoveAt(i);
        return copy;
    }
}

Then either add the converter to JsonSerializerOptions.Converters as follows:

var options = new JsonSerializerOptions
{
    Converters = { new NullTextValueForNullObjectConverter<Config>() },
    // Other options as required
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    PropertyNameCaseInsensitive = true
};
var webResponseWithConfig = JsonSerializer.Deserialize<WebResponse>(jsonWithConfig, options);

Or apply to the Config property directly as follows:

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]   
[JsonConverter(typeof(NullTextValueForNullObjectConverter<Config>))]
public Config Config { get; set; }  

Note that there does not appear to be a way currently to generate a default serialization of you apply the converter directly to the Config type. As such, I don't recommend doing it.

Demo fiddle here.

CodePudding user response:

If you cannot fix the JSON source then in this particular case i would recommend to replace "null" with null using a c# string replace function

json = json.Replace("\"null\"","null");
  • Related