Related
I would like to pass a parameter to the Json converter at the time of deserialization. At the same time, I would like the converter to execute only for the properties indicated by the attribute.
public class Contract
{
[JsonConverter(typeof(MyJsonConverter))]
public string Property { get; set; }
}
string parameter = "value";
var jsonSerializerSettings = new JsonSerializerSettings
{
Converters = { new MyJsonConverter(parameter) },
};
var contract = JsonConvert.DeserializeObject<Contract>(json, jsonSerializerSettings);
public class MyJsonConverter : JsonConverter
{
private readonly string _parameter;
public MyJsonConverter(string parameter)
{
_parameter = parameter;
}
public override bool CanConvert(Type objectType)
{
//
}
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
// use _parameter here
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
//
}
}
I know that the JsonConverter attribute accepts parameters for the converter, but then I would have to add one parameter to the Contract class permanently.
[JsonConverter(typeof(MyJsonConverter), <parameters>)]
I would like the parameters to be dynamically provided at the time of deserialization - how do I achieve this?
You can use StreamingContext.Context from JsonSerializerSettings.Context to pass data into a JsonConverter.
First, define the following interface and classes to cache data, keyed by System.Type, inside a StreamingContext:
public static class StreamingContextExtensions
{
public static StreamingContext AddTypeData(this StreamingContext context, Type type, object? data)
{
var c = context.Context;
IStreamingContextTypeDataDictionary dictionary;
if (context.Context == null)
dictionary = new StreamingContextTypeDataDictionary();
else if (context.Context is IStreamingContextTypeDataDictionary d)
dictionary = d;
else
throw new InvalidOperationException(string.Format("context.Context is already populated with {0}", context.Context));
dictionary.AddData(type, data);
return new StreamingContext(context.State, dictionary);
}
public static bool TryGetTypeData(this StreamingContext context, Type type, out object? data)
{
IStreamingContextTypeDataDictionary? dictionary = context.Context as IStreamingContextTypeDataDictionary;
if (dictionary == null)
{
data = null;
return false;
}
return dictionary.TryGetData(type, out data);
}
}
public interface IStreamingContextTypeDataDictionary
{
public void AddData(Type type, object? data);
public bool TryGetData(Type type, out object? data);
}
class StreamingContextTypeDataDictionary : IStreamingContextTypeDataDictionary
{
readonly Dictionary<Type, object?> dictionary = new ();
public void AddData(Type type, object? data) => dictionary.Add(type, data);
public bool TryGetData(Type type, out object? data) => dictionary.TryGetValue(type, out data);
}
Then rewrite MyConverter as follows:
public class MyJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => objectType == typeof(string);
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
// Grab parameter from serializer.Context. Use some default value (here "") if not present.
var _parameter = serializer.Context.TryGetTypeData(typeof(MyJsonConverter), out var s) ? (string?)s : "";
// Use _parameter as required, e.g.
return _parameter + (string?)JToken.Load(reader);
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) =>
writer.WriteValue((string)value!);
}
And you will be able to deserialize as follows:
var _parameter = "my runtime parameter: ";
var settings = new JsonSerializerSettings
{
Context = new StreamingContext(StreamingContextStates.All)
.AddTypeData(typeof(MyJsonConverter), _parameter),
// Add any other required customizations,
};
var contract = JsonConvert.DeserializeObject<Contract>(json, settings);
Notes:
The data cached inside the StreamingContext is keyed by type so that multiple converters could access cached data inside without interfering with each other. The type used should be the converter type, not the property type.
Demo fiddle #1 here.
Honestly though I don't recommend this design. StreamingContext is unfamiliar to current .NET programmers (it's a holdover from binary serialization) and it feels completely surprising to use it to pass data deep down into some JsonConverter.ReadJson() method.
As an alternative, you might consider creating a custom contract resolver that replaces the default MyJsonConverter applied at compile time with a different instance that has the required parameters.
First, define the following contract resolver:
public class ConverterReplacingContractResolver : DefaultContractResolver
{
readonly Dictionary<(Type type, string name), JsonConverter?> replacements;
public ConverterReplacingContractResolver(IEnumerable<KeyValuePair<(Type type, string name), JsonConverter?>> replacements) =>
this.replacements = (replacements ?? throw new ArgumentNullException()).ToDictionary(r => r.Key, r => r.Value);
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
if (member.DeclaringType != null && replacements.TryGetValue((member.DeclaringType, member.Name), out var converter))
property.Converter = converter;
return property;
}
}
Then modify MyJsonConverter so it has a default constructor with a default value for _parameter:
public class MyJsonConverter : JsonConverter
{
private readonly string _parameter;
public MyJsonConverter() : this("") { }
public MyJsonConverter(string parameter) => this._parameter = parameter;
public override bool CanConvert(Type objectType) => objectType == typeof(string);
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) =>
_parameter + (string?)JToken.Load(reader);
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) =>
writer.WriteValue((string)value!);
}
And now you will be able to deserialize as follows:
var _parameter = "my runtime parameter: ";
var replacementsConverters = new KeyValuePair<(Type type, string name), JsonConverter?> []
{
new((typeof(Contract), nameof(Contract.Property)), new MyJsonConverter(_parameter)),
};
var resolver = new ConverterReplacingContractResolver(replacementsConverters)
{
// Add any other required customizations, e.g.
//NamingStrategy = new CamelCaseNamingStrategy()
};
var settings = new JsonSerializerSettings
{
ContractResolver = resolver,
// Add other settings as required,
};
var contract = JsonConvert.DeserializeObject<Contract>(json, settings);
Demo fiddle #2 here.
From an external webservice i receive either
// jsonWithConfig
// property config is an object {}
{"config":{"c1":"All","c2":"is peachy"},"message":"We found a config object"}
// jsonNoConfig
// property config is string with the value null
{"config":"null","message":"Config is null"}
I want to deserialize the json into these types
public class WebResponse
{
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Config Config { get; set; }
public string Message { get; set; }
// i also tried dynamic but
// a) this is not what i want
// b) it resulted in RuntimeBinderException
// public dynamic Config { get; set; }
}
public class Config
{
public string C1 { get; set; }
public string C2 { get; set; }
}
What have i tried?
From How to ignore properties with System.Text.Json i started with JsonSerializer.Deserialize from System.Text.Json
var options = new JsonSerializerOptions{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNameCaseInsensitive = true
};
string jsonWithConfig =
{"config":{"c1":"All","c2":"is peachy"},"message":"We found a Config"}
WebResponse webResponseWithConfig =
JsonSerializer.Deserialize<WebResponse>(jsonWithConfig, options);
This works for jsonWithConfig which is no surprise since the json can be deserilized to the type WebResponse.
What is the error?
I hoped that using JsonSerializerOptions.DefaultIgnoreCondition would work for jsonNoConfig = {"config":"null","message":"Config is null"}.
But deserialization of jsonNoConfig fails with DeserializeUnableToConvertValue
string jsonNoConfig =
{"config":"null","message":"Config is null"}
WebResponse webResponseNoConfig =
JsonSerializer.Deserialize<WebResponse>(jsonNoConfig, options);
Questions
How can i deserialize jsonNoConfig?
What must i do?
Update
MySkullCaveIsADarkPlace pointed out that config should have the value null and not "null". After changing this the code above works as expected.
But is there a way to handle null with quotation marks like {"config":"null", ...} as well?
Full stack trace
The stack trace inside linqpad shows this
at System.Text.Json.ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(Type
propertyType)
at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader&
reader, Type typeToConvert, JsonSerializerOptions options, ReadStack&
state, T& value)
at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader&
reader, Type typeToConvert, JsonSerializerOptions options, ReadStack&
state, T& value)
at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.ReadJsonAndSetMember(Object
obj, ReadStack& state, Utf8JsonReader& reader)
at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader&
reader, Type typeToConvert, JsonSerializerOptions options, ReadStack&
state, T& value)
at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader&
reader, Type typeToConvert, JsonSerializerOptions options, ReadStack&
state, T& value)
at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader&
reader, JsonSerializerOptions options, ReadStack& state)
at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan1 utf8Json, JsonTypeInfo jsonTypeInfo, Nullable1 actualByteCount)
at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1
json, JsonTypeInfo jsonTypeInfo)
at System.Text.Json.JsonSerializer.Deserialize[TValue](String json,
JsonSerializerOptions options) at UserQuery.Main(), line 13
LINQPad program
This linqpad-program has all the code needed
// Linqpad program
void Main()
{
string jsonWithConfig = "{\"config\":{\"c1\":\"All\",\"c2\":\"is peachy\"},\"message\":\"We found a Config\"}";
string jsonNoConfig = "{\"config\":\"null\",\"Message\":\"Config is null\"}";
var options = new JsonSerializerOptions{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNameCaseInsensitive = true
};
WebResponse webResponseWithConfig = JsonSerializer.Deserialize<WebResponse>(jsonWithConfig, options);
webResponseWithConfig.Dump();
WebResponse webResponseNoConfig = JsonSerializer.Deserialize<WebResponse>(jsonNoConfig, options);
webResponseNoConfig.Dump();
}
// custom types
public class WebResponse
{
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Config Config { get; set; }
public string Message { get; set; }
}
public class Config
{
public string C1 { get; set; }
public string C2 { get; set; }
}
As explained in comments by MySkullCaveIsADarkPlace, your problem is that the JSON value "null"
"config":"null"
Is not null. It is a non-null string value containing the characters null. A null value looks like:
"config":null // Notice there are no quotes around the text
For confirmation, see the original JSON proposal.
If you cannot fix the JSON to represent null values properly, you will need to write a custom JsonConverter that checks for a "null" text value and returns null if present. If not present, the converter should proceed with default deserialization.
The question How to use default serialization in a custom System.Text.Json JsonConverter? has this answer which provides a DefaultConverterFactory<T>. Grab it and subclass it as follows:
NullTextValueForNullObjectConverter
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();
// Or use StringComparison.Ordinal if you are sure the
// text "null" will always be lowercase
if (string.Equals(s, NullValue, StringComparison.OrdinalIgnoreCase))
return null;
}
return (T)JsonSerializer.Deserialize(ref reader, typeToConvert, modifiedOptions);
}
}
DefaultConverterFactory
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);
}
JsonSerializerExtensions
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;
}
}
Then either add the converter to JsonSerializerOptions.Converters as follows:
var options = new JsonSerializerOptions
{
Converters = { new NullTextValueForNullObjectConverter<Config>() },
// Other options as required
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNameCaseInsensitive = true
};
var webResponseWithConfig = JsonSerializer.Deserialize<WebResponse>(jsonWithConfig, options);
Or apply to the Config property directly as follows:
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(NullTextValueForNullObjectConverter<Config>))]
public Config Config { get; set; }
Note that there does not appear to be a way currently to generate a default serialization of you apply the converter directly to the Config type. As such, I don't recommend doing it.
Demo fiddle here.
If you cannot fix the JSON source then in this particular case i would recommend to replace "null" with null using a c# string replace function
json = json.Replace("\"null\"","null");
NB: I am using System.Text.Json not JSON.NET for which there is similar question that has been answered. I need the same answer but for System.Text.Json. I would like to write my class to Json and ensure that it is human readable. To do this I have set the indent to true in options. However, the class contains a List<double> property which I don't want to indent as it makes the file very long.
So I have this:
public class ThingToSerialize
{
public string Name {get;set;}
//other properties here
public List<double> LargeList {get;set;}
};
var thing = new ThingToSerialize {Name = "Example", LargeList = new List<double>{0,0,0}};
var options = new JsonSerializerOptions
{
WriteIndented = true
};
options.Converters.Add(new DontIndentArraysConverter());
var s = JsonSerializer.Serialize(thing, options);
and I want it to serialize like this:
{
"Name": "Example",
"LargeList ": [0,0,0]
}
Not this (or something along these lines):
{
"Name": "Example",
"LargeList ": [
0,
0,
0
]
}
I have written a JsonConverter to achieve this:
public class DontIndentArraysConverter : JsonConverter<List<double>>
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(List<double>);
}
public override List<double> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return JsonSerializer.Deserialize<List<double>>(reader.GetString());
}
public override void Write(Utf8JsonWriter writer, List<double> value, JsonSerializerOptions options)
{
var s = JsonSerializer.Serialize(value);
writer.WriteStringValue(s);
}
}
However this writes the array as a string which I don't really want. Whats the best approach for this?
i.e. you get "[1,2,3]" instead of [1,2,3]
Secondly, the writer object that is passed into the Write function has an Options property but this cannot be changed. So if I write the array out manually using the writer object, it is indented.
Thanks for your idea, I wrote a wrapper based on the Converter hint
The idea is write a temp value during conversion, put correct array to a temp dict and replace them later
It's a bit late for you but maybe it can help other guys
public class CustomSerializer : IDisposable
{
private readonly Dictionary<string, string> _replacement = new Dictionary<string, string>();
public string Serialize<T>(T obj)
{
var converterForListInt = new DontIndentArraysConverterForListInt(_replacement);
var options = new JsonSerializerOptions
{
IgnoreNullValues = true,
WriteIndented = true
};
options.Converters.Add(converterForListInt);
var json = JsonSerializer.Serialize(obj, options);
foreach (var (k, v) in _replacement)
json = json.Replace(k, v);
return json;
}
public void Dispose()
{
_replacement.Clear();
}
public class DontIndentArraysConverterForListInt : JsonConverter<List<int>>
{
private readonly Dictionary<string, string> _replacement;
public DontIndentArraysConverterForListInt(Dictionary<string, string> replacement)
{
_replacement = replacement;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(List<int>);
}
public override List<int> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return JsonSerializer.Deserialize<List<int>>(reader.GetString());
}
public override void Write(Utf8JsonWriter writer, List<int> value, JsonSerializerOptions options)
{
if (value.Count > 0)
{
var key = $"TMP_{Guid.NewGuid().ToString()}";
var sb = new StringBuilder();
sb.Append('[');
foreach (var i in value)
{
sb.Append(i);
sb.Append(',');
}
sb.Remove(sb.Length - 1, 1); // trim last ,
sb.Append(']');
_replacement.Add($"\"{key}\"", sb.ToString());
//
writer.WriteStringValue(key);
}
else
{
// normal
writer.WriteStartArray();
writer.WriteEndArray();
}
}
}
}
Encountered this old question while trying to do the same thing for a List<string>. With .NET 6 (or at least package System.Text.Json v6.0.0.0) and later, this is now possible directly with System.Text.Json and the Utf8JsonWriter method WriteRawValue.
class ListDoubleSingleLineConverter : JsonConverter<List<double>>
{
//Read override omitted
public override void Write(Utf8JsonWriter writer, List<double> value, JsonSerializerOptions options)
{
writer.WriteRawValue(
String.Concat(
"[ ",
// Depending on your values, you might want to use LINQ .Select() and String.Format() to control the output
String.Join(", ", value),
" ]"));
}
}
Of note from the documentation:
When using this method, the input content will be written to the writer destination as-is, unless validation fails (when it is enabled).
(For my List<string> case, this meant transforming value with value.Select(v => String.Concat("\"", v, "\"")) before String.Join, as otherwise the strings were emitted unquoted; when using WriteRawValue you assume responsibility for the supplied argument being a well-formed JSON fragment.)
Tested with:
JsonSerializerOptions options = new JsonSerializerOptions()
{
WriteIndented = true,
Converters = { new ListDoubleSingleLineConverter() },
};
var test = new
{
Name = "Example",
LargeList = new List<double>() { 1.0, 1.1, 1.2, 1.3 },
OtherProperty = 27.0,
};
Console.WriteLine(JsonSerializer.Serialize(test, options));
Output:
{
"Name": "Example",
"LargeList": [ 1, 1.1, 1.2, 1.3 ],
"OtherProperty": 27
}
In the process of upgrading to ASP.NET Core 5, we have encountered a situation where we need to serialize and return a Json.NET JObject (returned by some legacy code we can't yet change) using System.Text.Json. How can this be done in a reasonably efficient manner, without re-serializing and re-parsing the JSON to a JsonDocument or reverting back to Json.NET completely via AddNewtonsoftJson()?
Specifically, say we have the following legacy data model:
public class Model
{
public JObject Data { get; set; }
}
When we return this from ASP.NET Core 5.0, the contents of the "value" property get mangled into a series of empty arrays. E.g.:
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 outputJson = JsonSerializer.Serialize(model);
Console.WriteLine(outputJson);
Assert.IsTrue(JToken.DeepEquals(JToken.Parse(inputJson), JToken.Parse(outputJson)[nameof(Model.Data)]));
Fails, and generates the following incorrect JSON:
{"Data":{"value":[[[],[],[],[],[],[],[],[],[],[]]]}}
How can I correctly serialize the JObject property with System.Text.Json? Note that the JObject can be fairly large so we would prefer to stream it out rather than format it to a string and parse it again from scratch into a JsonDocument simply to return it.
It is necessary to create a custom JsonConverterFactory to serialize a Json.NET JToken hierarchy to JSON using System.Text.Json.
Since the question seeks to avoid re-serializing the entire JObject to JSON just to parse it again using System.Text.Json, the following converter descends the token hierarchy recursively writing each individual value out to the Utf8JsonWriter:
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)
{
// This could be substantially optimized for memory use by creating code to read from a Utf8JsonReader and write to a JsonWriter (specifically a JTokenWriter).
// We could just write the JsonDocument to a string, but System.Text.Json works more efficiently with UTF8 byte streams so write to one of those instead.
using var doc = JsonDocument.ParseValue(ref reader);
using var ms = new MemoryStream();
using (var writer = new Utf8JsonWriter(ms))
doc.WriteTo(writer);
ms.Position = 0;
using (var sw = new StreamReader(ms))
using (var jw = new Newtonsoft.Json.JsonTextReader(sw))
{
return Newtonsoft.Json.JsonSerializer.CreateDefault(settings).Deserialize<TJToken>(jw);
}
}
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;
}
}
}
}
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);
}
}
Then the unit test in the question should be modified to use the following JsonSerializerOptions:
var options = new JsonSerializerOptions
{
Converters = { new JTokenConverterFactory() },
};
var outputJson = JsonSerializer.Serialize(model, options);
Notes:
The converter implements deserialization of JToken types as well as serialization, however since that wasn't a strict requirement of the question, it simply reads the entire JSON hierarchy into a JsonDocument, outputs it to a MemoryStream and re-parses it using Json.NET.
Newtonsoft's JsonSerializerSettings may be passed to customize settings such as FloatParseHandling or DateFormatHandling during deserialization.
To add JTokenConverterFactory to the ASP.NET Core serialization options, see Configure System.Text.Json-based formatters.
Demo fiddle with some basic tests here: fiddle #1.
A prototype version that implements deserialization by streaming from a Utf8JsonReader to a JsonWriter without loading the entire JSON value into a JsonDocument can be found here: fiddle #2.
I have a complex type that is to be serialized as a string. I have this working as desired using IXmlSerializable and am now trying to get it to work for JSON messages as well. Currently I get it to serialize via the DataContractSerializer to the following:
<MyXmlRootElement>_value's contents</MyXmlRootElement>
For the JSON output I would like to just receive a JSON string with the _value's contents.
This type implements more than described below to also affect the generated wsdl through a few other attributes that hook into our web services framework. So the generated wsdl/xsd all looks great, it just isn't giving me the desired JSON.
[XmlRoot(ElementName = "MyXmlRootElement", Namespace = "MyNamespace"]
public class ComplexType : IXmlSerializable
{
private string _value;
public ComplexType(string value) { _value = value; }
#region IXmlSerialiable Implementation
public XmlSchema GetSchema() { return null; }
public void ReadXml(XmlReader reader)
{
_value = reader.ReadString();
}
public void WriteXml(XmlWriter writer)
{
writer.WriteString(_value);
}
#endregion
}
I tried implementing ISerializable and this does affect the generated JSON, but it still places it in a complex (object) type, i.e, { "ValueKey": "_value's contents" }. Any idea how I can get it to serialize to a pure string, no curly braces?
The solution was easier than expected. You can register a JsonConverter with GlobalConfiguration. I made an abstract wrapper around JsonConverter as below, the idea for which came from the following SO thread: How to implement custom JsonConverter in JSON.NET to deserialize a List of base class objects?.
public abstract class CustomJsonConverter<T, TResult> : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// Load token from stream
var token = JToken.Load(reader);
// Create target object based on token
var target = Create(objectType, token);
var targetType = target.GetType();
if (targetType.IsClass && targetType != typeof(string))
{
// Populate the object properties
var tokenReader = token.CreateReader();
CopySerializerSettings(serializer, tokenReader);
serializer.Populate(token.CreateReader(), target);
}
return target;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
T typedValued = (T)value;
TResult valueToSerialize = Convert(typedValued);
serializer.Serialize(writer, valueToSerialize);
}
public override bool CanConvert(Type objectType)
{
return typeof (T) == objectType;
}
protected virtual T Create(Type type, JObject jObject)
{
// reads the token as an object type
if (typeof(TResult).IsClass && typeof(T) != typeof(string))
{
return Convert(token.ToObject<TResult>());
}
var simpleValue = jObject.Value<TResult>();
return Convert(simpleValue);
}
protected abstract TResult Convert(T value);
protected abstract T Convert(TResult value);
private static void CopySerializerSettings(JsonSerializer serializer, JsonReader reader)
{
reader.Culture = serializer.Culture;
reader.DateFormatString = serializer.DateFormatString;
reader.DateTimeZoneHandling = serializer.DateTimeZoneHandling;
reader.DateParseHandling = serializer.DateParseHandling;
reader.FloatParseHandling = serializer.FloatParseHandling;
}
}
You can then use this to do something like the following
public class DateTimeToStringJsonConverter : CustomJsonConverter<DateTime, string>
{
protected override string Convert(DateTime value)
{
return value.ToString();
}
protected override DateTime Convert(string value)
{
return DateTime.Parse(value);
}
}
And then finally register an instance with GlobalConfiguration in Global.asax.cs
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new DateTimeToStringJsonConverter());
Any thoughts/ideas/opinions? Thanks!
EDIT: 2018.10.16 - Updated code to handle value types as the source type and to copy serializer settings over thanks to the following comment from the original SO answer:
NOTE: This solution is all over the internet, but has a flaw that manifests itself in rare occasions. The new JsonReader created in the ReadJson method does not inherit any of the original reader's configuration values (Culture, DateParseHandling, DateTimeZoneHandling, FloatParseHandling, etc...). These values should be copied over before using the new JsonReader in serializer.Populate().
import this namespace System.Web.Script.Serialization;
string SerializeObject()
{
var objs = new List<Test>()
var objSerialized = new JavaScriptSerializer();
return objSerialized .Serialize(objs);
}
I use as example a List but you will use your object instead.