CSharp · Console
//https://stackoverflow.com/questions/65430420/how-to-use-default-serialization-in-a-custom-system-text-json-jsonconverter
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Nodes;
using Newtonsoft.Json.Linq;
using NUnit.Framework; // Throws NUnit.Framework.AssertionException
//https://stackoverflow.com/questions/74856371/c-sharp-jsonserializer-deserialize-fails-if-property-has-null-value-despite-json
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, modifiedOptions);
}
}
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;
}
}
class TestClass
{
public static void Test()
{
foreach (var jsonWithConfig in GetJson())
{
Console.WriteLine(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();
var json2 = JsonSerializer.Serialize(webResponseWithConfig , options);
Console.WriteLine(json2);
Console.WriteLine();
// Assert that webResponseWithConfig was properly populated.
var jsonWithConfigNode = JsonNode.Parse(jsonWithConfig);
var configOriginalJson = JsonSerializer.Serialize(jsonWithConfigNode["config"]);
Assert.AreEqual(webResponseWithConfig.Message, jsonWithConfigNode["message"].ToString());
if (webResponseWithConfig.Config == null)
{
Assert.That(configOriginalJson == "null" || configOriginalJson == "\"null\"");
}
else if (webResponseWithConfig.Config != null)
{
var options2 = new JsonSerializerOptions
{
Converters = { new NullTextValueForNullObjectConverter<Config>() },
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // The incoming JSON was actually camel case
};
Assert.AreEqual(configOriginalJson, JsonSerializer.Serialize(webResponseWithConfig.Config, options2));
}
}
}
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 Program
{
public static void Main()
{
Console.WriteLine("Environment version: {0} ({1}), {2}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription, Environment.Version, Environment.OSVersion);
Console.WriteLine("System.Text.Json version: " + typeof(JsonSerializer).Assembly.FullName);
Console.WriteLine();
try
{
TestClass.Test();
}
catch (Exception ex)
{
Console.WriteLine("Failed with unhandled exception: ");
Console.WriteLine(ex);
throw;
}
}
}