skip to Main Content

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 How to ignore properties with System.Text.Json i started with JsonSerializer.Deserialize from System.Text.Json

var options = new JsonSerializerOptions{
     DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
     PropertyNameCaseInsensitive = true
};
    
string jsonWithConfig = 
       {"config":{"c1":"All","c2":"is peachy"},"message":"We found a Config"}   
WebResponse webResponseWithConfig = 
            JsonSerializer.Deserialize<WebResponse>(jsonWithConfig, options);

This works for jsonWithConfig which is no surprise since the json can be deserilized to the type WebResponse.

What is the error?

I hoped that using JsonSerializerOptions.DefaultIgnoreCondition would work for jsonNoConfig = {"config":"null","message":"Config is null"}.
But deserialization of jsonNoConfig fails with DeserializeUnableToConvertValue

string jsonNoConfig = 
       {"config":"null","message":"Config is null"} 
WebResponse webResponseNoConfig = 
            JsonSerializer.Deserialize<WebResponse>(jsonNoConfig, options); 

Questions

  • How can i deserialize jsonNoConfig?
  • What must i do?

Update

MySkullCaveIsADarkPlace pointed out that config should have the value null and not "null". After changing this the code above works as expected.

But is there a way to handle null with quotation marks like {"config":"null", ...} as well?

Full stack trace

The stack trace inside shows this

at System.Text.Json.ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(Type
propertyType)

at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader&
reader, Type typeToConvert, JsonSerializerOptions options, ReadStack&
state, T& value)

at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader&
reader, Type typeToConvert, JsonSerializerOptions options, ReadStack&
state, T& value)

at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.ReadJsonAndSetMember(Object
obj, ReadStack& state, Utf8JsonReader& reader)

at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader&
reader, Type typeToConvert, JsonSerializerOptions options, ReadStack&
state, T& value)

at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader&
reader, Type typeToConvert, JsonSerializerOptions options, ReadStack&
state, T& value)

at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader&
reader, JsonSerializerOptions options, ReadStack& state)

at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan1 utf8Json, JsonTypeInfo jsonTypeInfo, Nullable1 actualByteCount)

at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1
json, JsonTypeInfo jsonTypeInfo)

at System.Text.Json.JsonSerializer.Deserialize[TValue](String json,
JsonSerializerOptions options) at UserQuery.Main(), line 13

LINQPad program

This -program has all the code needed


// Linqpad program
void Main()
{       
string jsonWithConfig = "{"config":{"c1":"All","c2":"is peachy"},"message":"We found a Config"}";
string jsonNoConfig   = "{"config":"null","Message":"Config is null"}";
    
var options = new JsonSerializerOptions{
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        PropertyNameCaseInsensitive = true
};
        
WebResponse webResponseWithConfig = JsonSerializer.Deserialize<WebResponse>(jsonWithConfig, options);
webResponseWithConfig.Dump();
WebResponse webResponseNoConfig = JsonSerializer.Deserialize<WebResponse>(jsonNoConfig, options);   
    webResponseNoConfig.Dump();
}

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

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

Linqpad demo

2

Answers


  1. 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, modifiedOptions);
        }
    }
    

    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.

    Login or Signup to reply.
  2. 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");
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search