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");
I am receiving a property that could either have null { "some_obj" : null } , an empty array [] { "some_obj" : [] }, or an object has is not null with some properties { "some_obj" : 'name' : 'nick' } as the value. In this context, an empty array should translate to null, so I created this converter.
public class MyConverter<T> : JsonConverter<T>
{
public override T? Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if(reader.TokenType == JsonTokenType.StartArray)
{
return default(T);
}
else if (reader.TokenType == JsonTokenType.StartObject)
{
return JsonSerializer.Deserialize<T>(ref reader, options);
}
else
{
return default(T);
}
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
=> JsonSerializer.Serialize(writer, (T?)value, options);
}
When the data coming in is { "some_obj" : [] }, the first if statement is true and I get the following the JsonException when returning default(T): "The converter {converter} read too much or not enough." Any idea of what I'm doing wrong? When the data is { "some_obj" : null } or { "some_obj" : 'name' : 'nick' } , it's working fine.
I'm using the attribute on a type in one my classes
public class SomeClass
{
[JsonConverter(typeof(MyConverter<SomeObj>))]
[JsonPropertyName("some_obj")]
public SomeObj? SomeObj{ get; set; }
}
public class SomeObj
{
[JsonPropertyName("name")]
public string? Name{ get; set; }
}
UPDATE
Making sure I ended on JsonTokenType.EndArray was the solution. Thanks Simon for the ticket reference
public override T? Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.StartObject)
{
return JsonSerializer.Deserialize<T>(ref reader, options);
}
if (reader.TokenType == JsonTokenType.StartArray)
{
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
{
return default(T);
}
}
}
throw new JsonException();
}
Inside JsonConverter<T>.Read(), if you want to ignore the contents of the current JSON token, you must advance the reader to end of the token, reading past any and all child tokens. The method Utf8JsonReader.Skip() does this:
public class MyConverter<T> : JsonConverter<T>
{
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
switch (reader.TokenType)
{
case JsonTokenType.StartObject:
return JsonSerializer.Deserialize<T>(ref reader, options);
default:
{
reader.Skip();
return default(T);
}
}
}
This method Skips the children of the current JSON token which is necessary when the token to be ignored is an array (and does nothing in the event the current token is a primitive value such as null).
Demo fiddle here.
Notes:
The version of the converter from your UPDATE will correctly skip past arrays without nested values such as [], but will not correctly skip past arrays with nested child arrays such as [[]]. In order to manually skip to the end of an array with child arrays you would need to track the CurrentDepth as well as the TokenType. Skip() takes care of this automatically.
If you want to ignore a property value or array entry, you must also use Utf8JsonReader.Skip() to consume that value. See How can I correctly skip an unknown property inside the JsonConverter<T>.Read() method of System.Text.Json? for details.
Edit: I made an issue at the .Net runtime repo yesterday which was closed to by "layomia" with this message: "Adding extension points like this comes with a performance cost at the lower-level reader and writer and does not present a good balance between perf and functionality/benefit. Providing such configuration is not on the System.Text.Json roadmap."
When setting JsonSerializerOptions.WriteIndented = true indentation looks like this when writing json...
{
"TILESET": "tilesets/HOW_TO_GET_TILESET_NAME_?",
"TILES": {
"TILE_1": {
"NAME": "auto_tile_18",
"TEXTURE_BOUNDS": [
304,
16,
16,
16
],
"SCREEN_BOUNDS": [
485,
159,
64,
64
]
}
}
}
Is there a way to change the automatic indentation to something like this...
{
"TILESET": "tilesets/HOW_TO_GET_TILESET_NAME_?",
"TILES":
{
"TILE_1":
{
"NAME": "auto_tile_18",
"TEXTURE_BOUNDS": [304, 16, 16,16],
"SCREEN_BOUNDS": [485, 159, 64, 64]
}
}
}
Update for .NET 6
While custom indentation rules are not supported by System.Text.Json, as of .NET 6 and later it is possible to disable indentation when serializing a particular member or type. By using Utf8JsonWriter.WriteRawValue(), you can create a custom JsonConverter that generates a default serialization for your value without indentation to a utf8 byte buffer, then writes the buffer to the incoming Utf8JsonWriter as-is.
First define the following converters:
public class NoIndentationConverter : NoIndentationConverter<object>
{
public override bool CanConvert(Type typeToConvert) => true;
}
public class NoIndentationConverter<T> : DefaultConverterFactory<T>
{
protected override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions modifiedOptions)
{
// TODO: investigate https://learn.microsoft.com/en-us/dotnet/api/microsoft.toolkit.highperformance.buffers.arraypoolbufferwriter-1
var bufferWriter = new ArrayBufferWriter<byte>();
using (var innerWriter = new Utf8JsonWriter(bufferWriter))
JsonSerializer.Serialize(innerWriter, value, modifiedOptions);
writer.WriteRawValue(bufferWriter.WrittenSpan, skipInputValidation : true);
}
protected override JsonSerializerOptions ModifyOptions(JsonSerializerOptions options) { (options = base.ModifyOptions(options)).WriteIndented = false; return options; }
}
public abstract class DefaultConverterFactory<T> : JsonConverterFactory
{
// Adapted from this answer https://stackoverflow.com/a/65430421/3744182
// To https://stackoverflow.com/questions/65430420/how-to-use-default-serialization-in-a-custom-system-text-json-jsonconverter
class DefaultConverter : JsonConverter<T>
{
readonly JsonSerializerOptions modifiedOptions;
readonly DefaultConverterFactory<T> factory;
public DefaultConverter(JsonSerializerOptions modifiedOptions, DefaultConverterFactory<T> factory) => (this.modifiedOptions, this.factory) = (modifiedOptions, factory);
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);
public override bool CanConvert(Type typeToConvert) => typeof(T).IsAssignableFrom(typeToConvert);
}
protected virtual JsonSerializerOptions ModifyOptions(JsonSerializerOptions options)
=> options.CopyAndRemoveConverter(this.GetType());
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 sealed override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => new DefaultConverter(ModifyOptions(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;
}
}
And now you can either apply NoIndentationConverter directly to your model (demo #1 here):
public partial class Tile1
{
[JsonPropertyName("NAME")]
public string Name { get; set; }
[JsonPropertyName("TEXTURE_BOUNDS")]
[JsonConverter(typeof(NoIndentationConverter))]
public List<long> TextureBounds { get; set; }
[JsonPropertyName("SCREEN_BOUNDS")]
[JsonConverter(typeof(NoIndentationConverter))]
public List<long> ScreenBounds { get; set; }
}
Or disable indentation for all List<long> values by adding NoIndentationConverter<List<long>> to JsonSerializerOptions.Converters as follows (demo #2 here):
var options = new JsonSerializerOptions
{
Converters = { new NoIndentationConverter<List<long>>() },
WriteIndented = true,
};
Both approaches result in your model being serialized as follows:
{
"TILESET": "tilesets/HOW_TO_GET_TILESET_NAME_?",
"TILES": {
"TILE_1": {
"NAME": "auto_tile_18",
"TEXTURE_BOUNDS": [304,16,16,16],
"SCREEN_BOUNDS": [485,159,64,64]
}
}
}
Notes:
If your arrays are very large, the temporary ArrayBufferWriter<byte> may consume substantial memory. You might look into using ArrayPoolBufferWriter<T> instead.
This approach does not work for a value that already has a custom JsonConverter applied. But you could rewrite that converter to use the same approach above.
You cannot disable indentation for a type by applying [JsonConverter(typeof(NoIndentationConverter))] directly to the type. Once a converter has been applied to a type, it is impossible to generate a "default" serialization using System.Text.Json. For details see this answer to How to use default serialization in a custom System.Text.Json JsonConverter?.
Original Answer
This is not possible currently with System.Text.Json (as of .NET 5). Let's consider the possibilities:
JsonSerializerOptions has no method to control indentation other than the Boolean property WriteIndented:
Gets or sets a value that defines whether JSON should use pretty printing.
Utf8JsonWriter has no method to modify or control indentation, as Options is a get-only struct-valued property.
In .Net Core 3.1, if I create a custom JsonConverter<T> for your TEXTURE_BOUNDS and SCREEN_BOUNDS lists and attempt set options.WriteIndented = false; during serialization, a System.InvalidOperationException: Serializer options cannot be changed once serialization or deserialization has occurred exception will be thrown.
Specifically, if I create the following converter:
class CollectionFormattingConverter<TCollection, TItem> : JsonConverter<TCollection> where TCollection : class, ICollection<TItem>, new()
{
public override TCollection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> JsonSerializer.Deserialize<CollectionSurrogate<TCollection, TItem>>(ref reader, options)?.BaseCollection;
public override void Write(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options)
{
var old = options.WriteIndented;
try
{
options.WriteIndented = false;
JsonSerializer.Serialize(writer, new CollectionSurrogate<TCollection, TItem>(value), options);
}
finally
{
options.WriteIndented = old;
}
}
}
public class CollectionSurrogate<TCollection, TItem> : ICollection<TItem> where TCollection : ICollection<TItem>, new()
{
public TCollection BaseCollection { get; }
public CollectionSurrogate() { this.BaseCollection = new TCollection(); }
public CollectionSurrogate(TCollection baseCollection) { this.BaseCollection = baseCollection ?? throw new ArgumentNullException(); }
public void Add(TItem item) => BaseCollection.Add(item);
public void Clear() => BaseCollection.Clear();
public bool Contains(TItem item) => BaseCollection.Contains(item);
public void CopyTo(TItem[] array, int arrayIndex) => BaseCollection.CopyTo(array, arrayIndex);
public int Count => BaseCollection.Count;
public bool IsReadOnly => BaseCollection.IsReadOnly;
public bool Remove(TItem item) => BaseCollection.Remove(item);
public IEnumerator<TItem> GetEnumerator() => BaseCollection.GetEnumerator();
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => ((IEnumerable)BaseCollection).GetEnumerator();
}
And the following data model:
public partial class Root
{
[JsonPropertyName("TILESET")]
public string Tileset { get; set; }
[JsonPropertyName("TILES")]
public Tiles Tiles { get; set; }
}
public partial class Tiles
{
[JsonPropertyName("TILE_1")]
public Tile1 Tile1 { get; set; }
}
public partial class Tile1
{
[JsonPropertyName("NAME")]
public string Name { get; set; }
[JsonPropertyName("TEXTURE_BOUNDS")]
[JsonConverter(typeof(CollectionFormattingConverter<List<long>, long>))]
public List<long> TextureBounds { get; set; }
[JsonPropertyName("SCREEN_BOUNDS")]
[JsonConverter(typeof(CollectionFormattingConverter<List<long>, long>))]
public List<long> ScreenBounds { get; set; }
}
Then serializing Root throws the following exception:
Failed with unhandled exception:
System.InvalidOperationException: Serializer options cannot be changed once serialization or deserialization has occurred.
at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_SerializerOptionsImmutable()
at System.Text.Json.JsonSerializerOptions.set_WriteIndented(Boolean value)
at CollectionFormattingConverter`2.Write(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options)
at System.Text.Json.JsonPropertyInfoNotNullable`4.OnWrite(WriteStackFrame& current, Utf8JsonWriter writer)
at System.Text.Json.JsonPropertyInfo.Write(WriteStack& state, Utf8JsonWriter writer)
at System.Text.Json.JsonSerializer.Write(Utf8JsonWriter writer, Int32 originalWriterDepth, Int32 flushThreshold, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.JsonSerializer.WriteCore(Utf8JsonWriter writer, Object value, Type type, JsonSerializerOptions options)
at System.Text.Json.JsonSerializer.WriteCore(PooledByteBufferWriter output, Object value, Type type, JsonSerializerOptions options)
at System.Text.Json.JsonSerializer.WriteCoreString(Object value, Type type, JsonSerializerOptions options)
at System.Text.Json.JsonSerializer.Serialize[TValue](TValue value, JsonSerializerOptions options)
Demo fiddle #1 here.
In .Net Core 3.1, if I create a custom JsonConverter<T> that creates a pre-formatted JsonDocument and then writes that out, the document will be reformatted as it is written.
I.e. if I create the following converter:
class CollectionFormattingConverter<TCollection, TItem> : JsonConverter<TCollection> where TCollection : class, ICollection<TItem>, new()
{
public override TCollection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> JsonSerializer.Deserialize<CollectionSurrogate<TCollection, TItem>>(ref reader, options)?.BaseCollection;
public override void Write(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options)
{
var copy = options.Clone();
copy.WriteIndented = false;
using var doc = JsonExtensions.JsonDocumentFromObject(new CollectionSurrogate<TCollection, TItem>(value), copy);
Debug.WriteLine("Preformatted JsonDocument: {0}", doc.RootElement);
doc.WriteTo(writer);
}
}
public static partial class JsonExtensions
{
public static JsonSerializerOptions Clone(this JsonSerializerOptions options)
{
if (options == null)
return new JsonSerializerOptions();
//In .Net 5 a copy constructor will be introduced for JsonSerializerOptions. Use the following in that version.
//return new JsonSerializerOptions(options);
//In the meantime copy manually.
var clone = new JsonSerializerOptions
{
AllowTrailingCommas = options.AllowTrailingCommas,
DefaultBufferSize = options.DefaultBufferSize,
DictionaryKeyPolicy = options.DictionaryKeyPolicy,
Encoder = options.Encoder,
IgnoreNullValues = options.IgnoreNullValues,
IgnoreReadOnlyProperties = options.IgnoreReadOnlyProperties,
MaxDepth = options.MaxDepth,
PropertyNameCaseInsensitive = options.PropertyNameCaseInsensitive,
PropertyNamingPolicy = options.PropertyNamingPolicy,
ReadCommentHandling= options.ReadCommentHandling,
WriteIndented = options.WriteIndented,
};
foreach (var converter in options.Converters)
clone.Converters.Add(converter);
return clone;
}
// Copied from this answer https://stackoverflow.com/a/62998253/3744182
// To https://stackoverflow.com/questions/62996999/convert-object-to-system-text-json-jsonelement
// By https://stackoverflow.com/users/3744182/dbc
public static JsonDocument JsonDocumentFromObject<TValue>(TValue value, JsonSerializerOptions options = default)
=> JsonDocumentFromObject(value, typeof(TValue), options);
public static JsonDocument JsonDocumentFromObject(object value, Type type, JsonSerializerOptions options = default)
{
var bytes = JsonSerializer.SerializeToUtf8Bytes(value, options);
return JsonDocument.Parse(bytes);
}
}
Fully indented JSON is generated despite the fact that the intermediate JsonDocument doc was serialized without indentation:
{
"TILESET": "tilesets/HOW_TO_GET_TILESET_NAME_?",
"TILES": {
"TILE_1": {
"NAME": "auto_tile_18",
"TEXTURE_BOUNDS": [
304,
16,
16,
16
],
"SCREEN_BOUNDS": [
485,
159,
64,
64
]
}
}
}
Demo fiddle #2 here.
And finally, in .Net Core 3.1, if I create a custom JsonConverter<T> that clones the incoming JsonSerializerOptions, modifies WriteIndented on the copy, then recursively serializes using the copied settings -- the modified value for WriteIndented is ignored.
Demo fiddle #3 here.
Apparently the JsonConverter architecture is going to be extensively enhanced in .Net 5 so you might re-test this option when it is released.
You might want to open an issue requesting this functionality, as there are multiple popular questions about how to do this with Json.NET (where it can be done with a converter):
How to apply indenting serialization only to some properties?
Newtonsoft inline formatting for subelement while serializing
Creating JSON without array indentation
Faced with the same problem. I need to write arrays in one row for json simplicity.
Latest version is here: https://github.com/micro-elements/MicroElements.Metadata/blob/master/src/MicroElements.Metadata.SystemTextJson/SystemTextJson/Utf8JsonWriterCopier.cs
Solution:
I Use reflection to create clone of Utf8JsonWriter with desired options (see class Utf8JsonWriterCopier.cs)
To check that API was not changed Clone calls Utf8JsonWriterCopier.AssertReflectionStateIsValid, also you can use it in your tests
Usage:
Create NotIndented copy of Utf8JsonWriter
Write array
Copy internal state back to original writer
Sample:
if (Options.WriteArraysInOneRow && propertyType.IsArray && writer.Options.Indented)
{
// Creates NotIndented writer
Utf8JsonWriter writerCopy = writer.CloneNotIndented();
// PropertyValue
JsonSerializer.Serialize(writerCopy, propertyValue.ValueUntyped, propertyType, options);
// Needs to copy internal state back to writer
writerCopy.CopyStateTo(writer);
}
Utf8JsonWriterCopier.cs
/// <summary>
/// Helps to copy <see cref="Utf8JsonWriter"/> with other <see cref="JsonWriterOptions"/>.
/// This is not possible with public API so Reflection is used to copy writer internals.
/// See also: https://stackoverflow.com/questions/63376873/in-system-text-json-is-it-possible-to-specify-custom-indentation-rules.
/// Usage:
/// <code>
/// if (Options.WriteArraysInOneRow and propertyType.IsArray and writer.Options.Indented)
/// {
/// // Create NotIndented writer
/// Utf8JsonWriter writerCopy = writer.CloneNotIndented();
///
/// // Write array
/// JsonSerializer.Serialize(writerCopy, array, options);
///
/// // Copy internal state back to writer
/// writerCopy.CopyStateTo(writer);
/// }
/// </code>
/// </summary>
public static class Utf8JsonWriterCopier
{
private class Utf8JsonWriterReflection
{
private IReadOnlyCollection<string> FieldsToCopyNames { get; } = new[] { "_arrayBufferWriter", "_memory", "_inObject", "_tokenType", "_bitStack", "_currentDepth" };
private IReadOnlyCollection<string> PropertiesToCopyNames { get; } = new[] { "BytesPending", "BytesCommitted" };
private FieldInfo[] Fields { get; }
private PropertyInfo[] Properties { get; }
internal FieldInfo OutputField { get; }
internal FieldInfo StreamField { get; }
internal FieldInfo[] FieldsToCopy { get; }
internal PropertyInfo[] PropertiesToCopy { get; }
public Utf8JsonWriterReflection()
{
Fields = typeof(Utf8JsonWriter).GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
Properties = typeof(Utf8JsonWriter).GetProperties(BindingFlags.Instance | BindingFlags.Public);
OutputField = Fields.FirstOrDefault(info => info.Name == "_output")!;
StreamField = Fields.FirstOrDefault(info => info.Name == "_stream")!;
FieldsToCopy = FieldsToCopyNames
.Select(name => Fields.FirstOrDefault(info => info.Name == name))
.Where(info => info != null)
.ToArray();
PropertiesToCopy = PropertiesToCopyNames
.Select(name => Properties.FirstOrDefault(info => info.Name == name))
.Where(info => info != null)
.ToArray();
}
public void AssertStateIsValid()
{
if (OutputField == null)
throw new ArgumentException("Field _output is not found. API Changed!");
if (StreamField == null)
throw new ArgumentException("Field _stream is not found. API Changed!");
if (FieldsToCopy.Length != FieldsToCopyNames.Count)
throw new ArgumentException("Not all FieldsToCopy found in Utf8JsonWriter. API Changed!");
if (PropertiesToCopy.Length != PropertiesToCopyNames.Count)
throw new ArgumentException("Not all FieldsToCopy found in Utf8JsonWriter. API Changed!");
}
}
private static readonly Utf8JsonWriterReflection _reflectionCache = new Utf8JsonWriterReflection();
/// <summary>
/// Checks that reflection API is valid.
/// </summary>
public static void AssertReflectionStateIsValid()
{
_reflectionCache.AssertStateIsValid();
}
/// <summary>
/// Clones <see cref="Utf8JsonWriter"/> with new <see cref="JsonWriterOptions"/>.
/// </summary>
/// <param name="writer">Source writer.</param>
/// <param name="newOptions">Options to use in new writer.</param>
/// <returns>New copy of <see cref="Utf8JsonWriter"/> with new options.</returns>
public static Utf8JsonWriter Clone(this Utf8JsonWriter writer, JsonWriterOptions newOptions)
{
AssertReflectionStateIsValid();
Utf8JsonWriter writerCopy;
// Get internal output to use in new writer
IBufferWriter<byte>? output = (IBufferWriter<byte>?)_reflectionCache.OutputField.GetValue(writer);
if (output != null)
{
// Create copy
writerCopy = new Utf8JsonWriter(output, newOptions);
}
else
{
// Get internal stream to use in new writer
Stream? stream = (Stream?)_reflectionCache.StreamField.GetValue(writer);
// Create copy
writerCopy = new Utf8JsonWriter(stream, newOptions);
}
// Copy internal state
writer.CopyStateTo(writerCopy);
return writerCopy;
}
/// <summary>
/// Clones <see cref="Utf8JsonWriter"/> and sets <see cref="JsonWriterOptions.Indented"/> to false.
/// </summary>
/// <param name="writer">Source writer.</param>
/// <returns>New copy of <see cref="Utf8JsonWriter"/>.</returns>
public static Utf8JsonWriter CloneNotIndented(this Utf8JsonWriter writer)
{
JsonWriterOptions newOptions = writer.Options;
newOptions.Indented = false;
return Clone(writer, newOptions);
}
/// <summary>
/// Clones <see cref="Utf8JsonWriter"/> and sets <see cref="JsonWriterOptions.Indented"/> to true.
/// </summary>
/// <param name="writer">Source writer.</param>
/// <returns>New copy of <see cref="Utf8JsonWriter"/>.</returns>
public static Utf8JsonWriter CloneIndented(this Utf8JsonWriter writer)
{
JsonWriterOptions newOptions = writer.Options;
newOptions.Indented = true;
return Clone(writer, newOptions);
}
/// <summary>
/// Copies internal state of one writer to another.
/// </summary>
/// <param name="sourceWriter">Source writer.</param>
/// <param name="targetWriter">Target writer.</param>
public static void CopyStateTo(this Utf8JsonWriter sourceWriter, Utf8JsonWriter targetWriter)
{
foreach (var fieldInfo in _reflectionCache.FieldsToCopy)
{
fieldInfo.SetValue(targetWriter, fieldInfo.GetValue(sourceWriter));
}
foreach (var propertyInfo in _reflectionCache.PropertiesToCopy)
{
propertyInfo.SetValue(targetWriter, propertyInfo.GetValue(sourceWriter));
}
}
/// <summary>
/// Clones <see cref="JsonSerializerOptions"/>.
/// </summary>
/// <param name="options">Source options.</param>
/// <returns>New instance of <see cref="JsonSerializerOptions"/>.</returns>
public static JsonSerializerOptions Clone(this JsonSerializerOptions options)
{
JsonSerializerOptions serializerOptions = new JsonSerializerOptions()
{
AllowTrailingCommas = options.AllowTrailingCommas,
WriteIndented = options.WriteIndented,
PropertyNamingPolicy = options.PropertyNamingPolicy,
DefaultBufferSize = options.DefaultBufferSize,
DictionaryKeyPolicy = options.DictionaryKeyPolicy,
Encoder = options.Encoder,
IgnoreNullValues = options.IgnoreNullValues,
IgnoreReadOnlyProperties = options.IgnoreReadOnlyProperties,
MaxDepth = options.MaxDepth,
PropertyNameCaseInsensitive = options.PropertyNameCaseInsensitive,
ReadCommentHandling = options.ReadCommentHandling,
};
foreach (JsonConverter jsonConverter in options.Converters)
{
serializerOptions.Converters.Add(jsonConverter);
}
return serializerOptions;
}
}
I use a custom converter to collapse Arrays and Below is an example for a hash set.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace MyProject.Core.Converters
{
public class HashSetConverter : JsonConverter<HashSet<string>?>
{
public override HashSet<string>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, HashSet<string>? value, JsonSerializerOptions options)
{
if (value is not null)
{
writer.WriteRawValue($"[\"{string.Join("\", \"", value)}\"]");
}
}
}
public class ArrayConverter : JsonConverter<int[]?>
{
public override int[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, int[]? value, JsonSerializerOptions options)
{
if (value is not null)
{
writer.WriteRawValue($"[{string.Join(", ", value)}]");
}
}
}
}
Then I setup my Serializer
var serializerOptions = new JsonSerializerOptions()
{
Converters = { new HashSetConverter(), new ArrayConverter() },
WriteIndented = true,
};
var json = JsonSerializer.Serialize(
new
{
Name = "auto_tile_18",
TEXTURE_BOUNDS = new int[]
{
304,
16,
16,
16,
},
},
serializerOptions);
Resulting Json
{
"Name": "auto_tile_18",
"TEXTURE_BOUNDS": [304, 16, 16, 16]
}
If you don't mind using an open source package or source file, take a look at FracturedJson ( nuget ), ( project home ). I wrote it specifically to create output that is easy to read without wasting tons of vertical space.
Here's what the output looks like for the original poster's data, using default options:
{
"TILESET": "tilesets/HOW_TO_GET_TILESET_NAME_?",
"TILES": {
"TILE_1": {
"NAME": "auto_tile_18",
"TEXTURE_BOUNDS": [304, 16, 16, 16],
"SCREEN_BOUNDS": [485, 159, 64, 64]
}
}
}
I have a struct that I use in my ViewModels in order to simplify the way they get automatically serialized. I want to the serialization of it to simply call the .ToString() method. Currently when it's serialized to JSON it gets turned into: "{}". Here is my Date class:
public struct Date
{
private DateTime _date;
public Date(DateTime date)
{
_date = date;
}
public static implicit operator Date(DateTime date) => new Date(date);
public override string ToString() => _date.ToString("yyyy-MM-dd");
}
I thought that there might be some sort of attribute that I can decorate the struct with or perhaps some interface that I can implement but it doesn't appear to help.
A Lot of times, this is required in a lot of Classes when the project is bigger.
We need the conversion to work both ways. A class should be serialized using the ToString() method, and the class should be deserialized from the String. We use the following convention.
Define marker interfaces to let classes explicitly adhere to the contract that they support Serialization using the ToString method and also support Deserialization from string to object instance.
/// <summary>
/// Interface to convert string to a type T
/// </summary>
public interface IStringToTypeConverter<out T>
{
T ConvertToType(string stringToConvertFrom);
}
/// <summary>
/// Marker Interface to let Serialization/Deserialization work on the ToString Method of the class, Rather than
/// calling on the Instance properties
/// </summary>
public interface ITypeToStringConverter
{
}
Next, Define the generic converter which will do the conversion (Serialization/Deserialization) for a class which implements the above interfaces.
public class ToStringJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
var isTypeImplementStringToTypeConverter = objectType.GetInterfaces().Any(x =>
x == typeof(ITypeToStringConverter) ||
(x.IsGenericType &&
x.GetGenericTypeDefinition() == typeof(IStringToTypeConverter<>)));
return isTypeImplementStringToTypeConverter;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString());
}
public override bool CanRead
{
get { return true; }
}
public override bool CanWrite
{
get { return true; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// Load the JSON for the Result into a JObject
var stringValue = reader.Value.ToString();
if (string.IsNullOrWhiteSpace(stringValue))
{
var jObject = JObject.Load(reader);
stringValue = jObject.ToString();
}
MethodInfo parse = objectType.GetMethod("ConvertToType");
if (parse != null)
{
var destinationObject = Activator.CreateInstance(objectType,stringValue);
return parse.Invoke(destinationObject, new object[] { stringValue });
}
throw new JsonException($"The {objectType.Name} type does not have a public ConvertToType(string) method.");
}
}
Lastly, Add the Converter to the startup class, passing it in the JSON Options
services.AddMvc().AddJsonOptions(options =>
{
options.SerializerSettings.Converters.Add(new ToStringJsonConverter());
})
Note: Please test the performance benhmarks for your code to see if it has any impact on your performance SLA.
After some further research it looks like MVC uses the JsonConverter attribute to serialize JsonResults. The code below accomplished what I was trying to do.
[JsonConverter(typeof(DateToString))]
public struct Date
{
private DateTime _date;
public Date(DateTime date)
{
_date = date;
}
public static implicit operator Date(DateTime date) => new Date(date);
public override string ToString() => _date.ToString("yyyy-MM-dd");
}
public class DateToString : JsonConverter
{
public override bool CanConvert(Type objectType) => objectType == typeof(Date);
public override bool CanRead => false;
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
=> throw new NotImplementedException();
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) =>
writer.WriteValue(value.ToString());
}