https://stackoverflow.com/questions/74856371/c-sharp-jsonserializer-deserialize-fails-if-property-has-null-value-despite-json

CSharp · Console

/Program.cs

//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;
			}
		}
	}
;