2

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

dbc
  • 104,963
  • 20
  • 228
  • 340
surfmuggle
  • 5,527
  • 7
  • 48
  • 77
  • 4
    Note that the config property in `{"config":"null","message":"Config is null"}` does **not** have null value. It has a **string** value composed of the characters `n`,`u`,`l`,`l` (an actual null value would not be in quotation marks). And since a string value is not convertible to an instance of type _Config_ you get an error... –  Dec 19 '22 at 22:15
  • 1
    This already helped. If i remove the quotes and change `"null"` to `null` the conversion seems to work. – surfmuggle Dec 19 '22 at 22:20
  • See update above - is there a way to handle `"null"` as well? – surfmuggle Dec 19 '22 at 22:47
  • I don't know. Neither System.Text.Json nor Newtonsoft.Json support the `"null"` string as a null value. Depending on the complexity of the json you have to deserialize and the amount of different places a `"null"` string representing a null value might appear in the json, possible solutions could range from a rather hackish yet simple string replacement in the json string before feeding it to the deserializer to writing JsonConverter(s) for the affected properties/types. –  Dec 19 '22 at 22:51
  • If writing and testing the respective JsonConverters sounds like too much or too complicated work, or if there are simply too many types/properties to cover and too many places in the json where such a `"null"` might appear, there is always contacting the admins of the web service and bitching and begging like your life depends on it as last resort (not sure if that is a feasible option for your situation, though)... –  Dec 19 '22 at 22:54

2 Answers2

2

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • This works great. For anyone who would like to test it and play around with it i copied the [code as linqpad program](https://gist.github.com/surfmuggle/e04d4699a3c6c305f4af721d3db0e73d) – surfmuggle Dec 20 '22 at 10:22
  • Could it be that the re-serialization omits the `Config`-object? I changed line 105 to this `string webResponse2Json = JsonSerializer.Serialize(webResponseWithConfig , options);` and line 107 to this `Console.WriteLine("webResponse2Json" + webResponse2Json);`. For `jsonWithConfig` i expected that the config property has this object `{"c1":"All","c2":"is peachy"}` but actual got an empty object for the config property `{"Config":{}, ...}` – surfmuggle Dec 20 '22 at 12:28
  • 1
    @surfmuggle - sorry, my mistake. I forgot to pass `modifiedOptions` to `(T)JsonSerializer.Deserialize(ref reader, typeToConvert, modifiedOptions);`. Fixed. – dbc Dec 20 '22 at 16:09
1

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");
Serge
  • 40,935
  • 4
  • 18
  • 45
  • 1
    [Be nice](https://stackoverflow.com/help/behavior) is quite easy if you try. This comment refers to the [first version of this answer](https://stackoverflow.com/posts/74857354/revisions) – surfmuggle Dec 20 '22 at 00:23