[Fork] How can I serialize a JToken to System.Text.Json?

CSharp · Console

/Program.cs

//https://dotnetfiddle.net/nKUORV
//https://stackoverflow.com/questions/65386083/how-can-i-serialize-a-newtonsoft-jtoken-to-json-using-system-text-json/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization.Formatters;
using System.IO;
using System.Data;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Reflection;
using System.Collections.ObjectModel;


using NUnit.Framework; // Throws NUnit.Framework.AssertionException

	using System.Text.Json;
	using System.Text.Json.Serialization;
	using Newtonsoft.Json.Linq;

	public class JTokenConverterFactory : JsonConverterFactory
	{
		// In case you need to set FloatParseHandling or DateFormatHandling
		readonly Newtonsoft.Json.JsonSerializerSettings settings;
		
		public JTokenConverterFactory() { }

		public JTokenConverterFactory(Newtonsoft.Json.JsonSerializerSettings settings) => this.settings = settings;
	
		public override bool CanConvert(Type typeToConvert) => typeof(JToken).IsAssignableFrom(typeToConvert);

		public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
		{
			var converterType = typeof(JTokenConverter<>).MakeGenericType(new [] { typeToConvert} );
			return (JsonConverter)Activator.CreateInstance(converterType, new object [] { options, settings } );
		}

		class JTokenConverter<TJToken> : JsonConverter<TJToken> where TJToken : JToken
		{
			readonly JsonConverter<bool> boolConverter;
			readonly JsonConverter<long> longConverter;
			readonly JsonConverter<double> doubleConverter;
			readonly JsonConverter<decimal> decimalConverter;
			readonly JsonConverter<string> stringConverter;
			readonly JsonConverter<DateTime> dateTimeConverter;
			readonly Newtonsoft.Json.JsonSerializerSettings settings;

			public override bool CanConvert(Type typeToConvert) => typeof(TJToken).IsAssignableFrom(typeToConvert);

			public JTokenConverter(JsonSerializerOptions options, Newtonsoft.Json.JsonSerializerSettings settings)
			{
				// Cache some converters for efficiency
				boolConverter = (JsonConverter<bool>)options.GetConverter(typeof(bool));
				stringConverter = (JsonConverter<string>)options.GetConverter(typeof(string));
				longConverter = (JsonConverter<long>)options.GetConverter(typeof(long));
				decimalConverter = (JsonConverter<decimal>)options.GetConverter(typeof(decimal));
				doubleConverter = (JsonConverter<double>)options.GetConverter(typeof(double));
				dateTimeConverter = (JsonConverter<DateTime>)options.GetConverter(typeof(DateTime));
				this.settings = settings;
			}
			
			public override TJToken Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
			{
				using (JTokenWriter newtonsoftWriter = new JTokenWriter())
				{
					ReadCore(ref reader, newtonsoftWriter, options, typeof(TJToken), Newtonsoft.Json.JsonSerializer.CreateDefault(settings));
					return (TJToken)newtonsoftWriter.Token;
				}
			}

			public override void Write(Utf8JsonWriter writer, TJToken value, JsonSerializerOptions options) =>
				// Optimize for memory use by descending the JToken hierarchy and writing each one out, rather than formatting to a string, parsing to a `JsonDocument`, then writing that.
				WriteCore(writer, value, options);
			
			void WriteCore(Utf8JsonWriter writer, JToken value, JsonSerializerOptions options)
			{
				if (value == null || value.Type == JTokenType.Null)
				{
					writer.WriteNullValue();
					return;
				}

				switch (value)
				{
					case JValue jvalue when jvalue.GetType() != typeof(JValue): // JRaw, maybe others
					default: // etc
						{
							// We could just format the JToken to a string, but System.Text.Json works more efficiently with UTF8 byte streams so write to one of those instead.
							using var ms = new MemoryStream();
							using (var tw = new StreamWriter(ms, leaveOpen : true))
							using (var jw = new Newtonsoft.Json.JsonTextWriter(tw))
							{
								value.WriteTo(jw);
							}
							ms.Position = 0;
							using var doc = JsonDocument.Parse(ms);
							doc.WriteTo(writer);
						}
						break;
					// Hardcode some standard cases for efficiency
					case JValue jvalue when jvalue.Value is bool v:
						boolConverter.WriteOrSerialize(writer, v, options);
						break;
					case JValue jvalue when jvalue.Value is string v:
						stringConverter.WriteOrSerialize(writer, v, options);
						break;
					case JValue jvalue when jvalue.Value is long v:
						longConverter.WriteOrSerialize(writer, v, options);
						break;
					case JValue jvalue when jvalue.Value is decimal v:
						decimalConverter.WriteOrSerialize(writer, v, options);
						break;
					case JValue jvalue when jvalue.Value is double v:
						doubleConverter.WriteOrSerialize(writer, v, options);
						break;
					case JValue jvalue when jvalue.Value is DateTime v:
						dateTimeConverter.WriteOrSerialize(writer, v, options);
						break;
					case JValue jvalue:
						JsonSerializer.Serialize(writer, jvalue.Value, options);
						break;
					case JArray array:
						{
							writer.WriteStartArray();
							foreach (var item in array)
								WriteCore(writer, item, options);
							writer.WriteEndArray();
						}
						break;
					case JObject obj:
						{
							writer.WriteStartObject();
							foreach (var p in obj.Properties())
							{
								writer.WritePropertyName(p.Name);
								WriteCore(writer, p.Value, options);
							}
							writer.WriteEndObject();
						}
						break;
				}
			}

			void ReadArrayCore(ref Utf8JsonReader reader, Newtonsoft.Json.JsonWriter newtonsoftWriter, JsonSerializerOptions options, Newtonsoft.Json.JsonSerializer newtonsoftSerializer)
			{
				if (reader.TokenType != JsonTokenType.StartArray)
					throw new JsonException();
				newtonsoftWriter.WriteStartArray();
				while (reader.Read())
				{
					switch (reader.TokenType)
					{
						default:
							ReadCore(ref reader, newtonsoftWriter, options, typeof(JToken), newtonsoftSerializer);
							break;
						
						case JsonTokenType.EndArray:
							newtonsoftWriter.WriteEndArray();
							return;
					}
				}
				throw new JsonException();
			}

			void ReadObjectCore(ref Utf8JsonReader reader, Newtonsoft.Json.JsonWriter newtonsoftWriter, JsonSerializerOptions options, Newtonsoft.Json.JsonSerializer newtonsoftSerializer)
			{
				if (reader.TokenType != JsonTokenType.StartObject)
					throw new JsonException();
				newtonsoftWriter.WriteStartObject();
				while (reader.Read())
				{
					switch (reader.TokenType)
					{
						case JsonTokenType.PropertyName:
							var name = reader.GetString();
							if (!reader.Read())
								throw new JsonException();
							newtonsoftWriter.WritePropertyName(name);
							ReadCore(ref reader, newtonsoftWriter, options, typeof(JToken), newtonsoftSerializer);
							break;
						
						case JsonTokenType.EndObject:
							newtonsoftWriter.WriteEndObject();
							return;
							
						default:
							throw new JsonException();
					}
				}
				throw new JsonException();
			}

			static void CheckType(Type extectedType, Type actualType, JsonTokenType tokenType)
			{
				if (!extectedType.IsAssignableFrom(actualType))
					throw new JsonException(string.Format("Expected type {0} cannot be created from token {1}", extectedType, tokenType));
			}

			void ReadCore(ref Utf8JsonReader reader, Newtonsoft.Json.JsonWriter newtonsoftWriter, JsonSerializerOptions options, Type extectedType, Newtonsoft.Json.JsonSerializer newtonsoftSerializer)
			{
				var t = reader.TokenType;
				switch (reader.TokenType)
				{
					case JsonTokenType.Comment:
						CheckType(extectedType, typeof(JValue), reader.TokenType);
						newtonsoftWriter.WriteComment(reader.GetString());
						break;
					case JsonTokenType.False:
						CheckType(extectedType, typeof(JValue), reader.TokenType);
						newtonsoftWriter.WriteValue(false);
						break;
					case JsonTokenType.True:
						CheckType(extectedType, typeof(JValue), reader.TokenType);
						newtonsoftWriter.WriteValue(true);
						break;
					case JsonTokenType.Null:
						CheckType(extectedType, typeof(JValue), reader.TokenType);
						newtonsoftWriter.WriteNull();
						break;
					case JsonTokenType.String:
						{
							// To ensure that DateTime values are recognized consistently with Json.NET conventions, we must invoke the Json.NET
							// serializer or parser using the incoming JsonSerializerSettings. See https://www.newtonsoft.com/json/help/html/DatesInJSON.htm
							CheckType(extectedType, typeof(JValue), reader.TokenType);
							using var newtonsoftReader = new Newtonsoft.Json.JsonTextReader(new StringReader(Newtonsoft.Json.JsonConvert.ToString(reader.GetString())));
							newtonsoftSerializer.Deserialize<JValue>(newtonsoftReader).WriteTo(newtonsoftWriter);
						}
						break;
					case JsonTokenType.Number:
						CheckType(extectedType, typeof(JValue), reader.TokenType);
						// For efficiency, see if the value is an integer convertible to long.
						if (reader.TryGetInt64(out var l))
							newtonsoftWriter.WriteValue(l);
						else
						{
							// Inconsistencies in numeric formats cause difficulties here.  System.Text.Json keeps the underlying byte representation while JsonReader parses
							// to the closest fit .Net numeric type (long, double, decimal or BigInteger).  Just get the raw JSON and let Newtonsoft handle the conversion.
							using var doc = JsonDocument.ParseValue(ref reader);
							using var newtonsoftReader = new Newtonsoft.Json.JsonTextReader(new StringReader(doc.RootElement.ToString()));
							newtonsoftSerializer.Deserialize<JValue>(newtonsoftReader).WriteTo(newtonsoftWriter);
						}
						break;
					case JsonTokenType.StartArray:
						CheckType(extectedType, typeof(JArray), reader.TokenType);
						ReadArrayCore(ref reader, newtonsoftWriter, options, newtonsoftSerializer);
						break;
					case JsonTokenType.StartObject:
						CheckType(extectedType, typeof(JObject), reader.TokenType);
						ReadObjectCore(ref reader, newtonsoftWriter, options, newtonsoftSerializer);
						break;
				}
			}
		}
	}

	public static class JsonExtensions
	{
		public static void WriteOrSerialize<T>(this JsonConverter<T> converter, Utf8JsonWriter writer, T value, JsonSerializerOptions options)
		{
			if (converter != null)
				converter.Write(writer, value, options);
			else
				JsonSerializer.Serialize(writer, value, options);
		}
	}

	public class Model
	{
		public JObject Data { get; set; }
	}

    class TestClass
    {
        public static void Test()
        {
			var inputJson = @"{""value"":[[null,true,false,1010101,1010101.10101,""hello"",""𩸽"",""\uD867\uDE3D"",""2009-02-15T00:00:00Z"",""\uD867\uDE3D\u0022\\/\b\f\n\r\t\u0121""]]}";
			var model = new Model { Data = JObject.Parse(inputJson) };
			var options = new JsonSerializerOptions
			{
				Converters = { new JTokenConverterFactory() },
			};
			var outputJson = JsonSerializer.Serialize(model, options);
			
			Console.WriteLine(outputJson);
			
			Assert.IsTrue(JToken.DeepEquals(JToken.Parse(inputJson), JToken.Parse(outputJson)[nameof(Model.Data)]));

			// And some additional tests
			foreach (var json in GetJson())
			{
				Test(JToken.Parse(json));
				TestRaw(json);
			}
		}	
		
		static void TestRaw(string originalJson)
		{
			var options = new JsonSerializerOptions
			{
				Converters = { new JTokenConverterFactory() },
				ReadCommentHandling = JsonCommentHandling.Skip,
			};
			var raw = new JRaw(originalJson);

			SerializeAndAssert(new JRaw(originalJson), options);
		}
		
		static void Test(JToken root)
		{
			var options = new JsonSerializerOptions
			{
				Converters = { new JTokenConverterFactory() },
			};

			var json2 = SerializeAndAssert(root, options);

			Console.WriteLine(json2);

			var root2 = JsonSerializer.Deserialize<JToken>(json2, options);

			try
			{
				Assert.IsTrue(AreEqual(root, root2), "AreEqual(root, root2)");
			}
			catch (Exception)
			{
				Console.WriteLine("Failed with root2:");
				Console.WriteLine(root2);
				throw;
			}
		}		

		static string SerializeAndAssert(JToken root, JsonSerializerOptions options)
		{
			var json1 = JsonSerializer.Serialize(root, options);
			var json2 = JsonSerializer.Serialize(root, root.GetType(), options);

			Assert.IsTrue(AreEqual(root, JToken.Parse(json1)), root?.ToString());
			Assert.AreEqual(json1, json2, "json1 == json2");

			return json1;
		}
		
		static bool IsNull(JToken token)
		{
			if (token == null)
				return true;
			if (token.Type == JTokenType.Null)
				return true;
			return false;
		}
		
		static bool AreEqual(JToken token1, JToken token2)
		{
			if (token1 == token2)
				return true;

			if (token1 is JRaw)
				token1 = JToken.Parse(token1.ToString());
			if (token2 is JRaw)
				token2 = JToken.Parse(token2.ToString());
			
			if (IsNull(token1) && IsNull(token2))
				return true;

			return JToken.DeepEquals(token1, token2);
		}
		
		public static IEnumerable<string> GetJson()
		{
			var primitives = new []
			{
				@"null",
				@"true",
				@"false",
				@"1010101",
				@"1010101.10101",
				@"""hello""",
				@"""𩸽""", // A character not in the basic multilingual plane
				@"""2009-02-15T00:00:00Z""", //https://www.newtonsoft.com/json/help/html/DatesInJSON.htm
				@"""\uD867\uDE3D\""\\\/\b\f\n\r\t\u0121""",
			};

			// Copied from https://json.org/example.html
			var examples = new []
			{
				@"{ 
    ""glossary"": {
        ""title"": ""example glossary"",
		""GlossDiv"": {
            ""title"": ""S"",
			""GlossList"": {
                ""GlossEntry"": {
                    ""ID"": ""SGML"",
					""SortAs"": ""SGML"",
					""GlossTerm"": ""Standard Generalized Markup Language"",
					""Acronym"": ""SGML"",
					""Abbrev"": ""ISO 8879:1986"",
					""GlossDef"": {
                        ""para"": ""A meta-markup language, used to create markup languages such as DocBook."",
						""GlossSeeAlso"": [""GML"", ""XML""]
                    },
					""GlossSee"": ""markup""
                }
            }
        }
    }
}",
				@"{""menu"": {
  ""id"": ""file"",
  ""value"": ""File"",
  ""popup"": {
    ""menuitem"": [
      {""value"": ""New"", ""onclick"": ""CreateNewDoc()""},
      {""value"": ""Open"", ""onclick"": ""OpenDoc()""},
      {""value"": ""Close"", ""onclick"": ""CloseDoc()""}
    ]
  }
}}"
				
			};
			
			var primitivesAndExamples = primitives.Concat(examples);
			
			var arrays = primitivesAndExamples.Select(s => "[[" + s + ", [" + s + "]]]").Concat(new [] { "[[" + string.Join(',', primitives) +"]]"});
			
			var objects = arrays.Select(s => "{" + "\"value\":" + s + "}");
			
			return primitivesAndExamples.Concat(arrays).Concat(objects);		
		}
	}

	public class Program
	{
		public static void Main()
		{
			Console.WriteLine("Environment version: {0} ({1})", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , GetNetCoreVersion());
			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;
			}
		}
		
		public static string GetNetCoreVersion()
		{
			//https://techblog.dorogin.com/how-to-detect-net-core-version-in-runtime-ecd65ad695be
			//
			var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly;
			var assemblyPath = assembly.Location.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
			int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App");
			if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2)
				return assemblyPath[netCoreAppIndex + 1];
			return null;
		}
	}
;