Created
December 20, 2022 10:21
-
-
Save surfmuggle/e04d4699a3c6c305f4af721d3db0e73d to your computer and use it in GitHub Desktop.
C# JsonSerializer.Deserialize fails if property has string value "null" despite JsonIgnoreCondition.WhenWritingNull
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
void Main() | |
{ | |
// Linqpad program to test the custom serializer to handle strings with value null: string foo = "\"null\""; | |
// based on this answer https://stackoverflow.com/a/74857003/819887 | |
try | |
{ | |
//TestClass.Test(); | |
foreach (var jsonWithConfig in GetJson()) | |
{ | |
jsonWithConfig.Dump("jsonWithConfig"); | |
var options = new JsonSerializerOptions | |
{ | |
Converters = { new NullTextValueForNullObjectConverter<Config>() }, | |
// Other options as required | |
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, | |
PropertyNameCaseInsensitive = true | |
}; | |
var webResponseWithConfig = JsonSerializer.Deserialize<WebResponse>(jsonWithConfig, options); | |
webResponseWithConfig.Dump("deserialized to webResponseWithConfig"); | |
var json2 = JsonSerializer.Serialize(webResponseWithConfig , options); | |
json2.Dump("serialized to webResponseWithConfig"); | |
} | |
} | |
catch (Exception ex) | |
{ | |
Console.WriteLine("Failed with unhandled exception: "); | |
Console.WriteLine(ex); | |
throw; | |
} | |
} | |
// You can define other methods, fields, classes and namespaces here | |
static IEnumerable<string> GetJson() => new []{ | |
""" | |
{"config":{"c1":"All","c2":"is peachy"},"message":"We found a config object"} | |
""", | |
""" | |
{"config":"null","message":"Config is \"null\""} | |
""" | |
}; | |
public class WebResponse | |
{ | |
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | |
[JsonConverter(typeof(NullTextValueForNullObjectConverter<Config>))] | |
public Config Config { get; set; } | |
public string Message { get; set; } | |
} | |
public class Config | |
{ | |
public string C1 { get; set; } | |
public string C2 { get; set; } | |
} | |
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(); | |
if (string.Equals(s, NullValue, StringComparison.OrdinalIgnoreCase)) // Or use StringComparison.Ordinal if you are sure the text "null" will always be lowercase | |
return null; | |
} | |
return (T)JsonSerializer.Deserialize(ref reader, typeToConvert); | |
} | |
} | |
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); | |
} | |
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; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
It seems that the deserialization omits the
Config
-object{config: {...}, ...}
value of property config is a not empty object{...}
{config: {}, ...}
value of config property is an empty object{}