CSharp · Console
//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;
}
}