Using .NET 4.8 Framework and System.Text.Json v.9.0, I was trying to add comments when serialize like in this post, with a custom converter
How do I add comments to Json.NET output?
public class JsonCommentConverter : JsonConverter
{
private readonly string _comment;
public JsonCommentConverter(string comment)
{
_comment = comment;
}
...
}
public class Person
{
[JsonConverter(typeof(JsonCommentConverter), "Name of the person")]
public string Name { get; set; }
[JsonConverter(typeof(JsonCommentConverter), "Age of the person")]
public int Age { get; set; }
}
Unfortunately, that was an old post that was specific to Newtonsoft. JsonConverterAttribute
from System.Text.Json doesn't allow arguments to be passed to the converter via attribute parameters.
I know that JSON standard doesn't allow comments, but is there a way to add them at serialize point?
Basically, this is what I want
{
"Name": "Jack"/*Name of the person*/,
"Age": 22/*Age of the person*/
}
While it is true that JsonConverterAttribute
does not support passing of constructor arguments, it is not sealed so you could create your own attribute that derives from it with the required comment as a parameter. Then override JsonConverterAttribute.CreateConverter(Type)
to return your JsonCommentConverter
with the comment passed into its constructor.
To do that, first create the following custom attribute:
public sealed class JsonCommentAttribute : JsonConverterAttribute
{
readonly string Comment;
public JsonCommentAttribute(string Comment) { this.Comment = Comment; }
public override JsonConverter CreateConverter (Type typeToConvert) { return new JsonCommentConverter(Comment); }
}
Next create JsonCommentConverter
as follows:
public class JsonCommentConverter : JsonCommentConverter<object>
{
public JsonCommentConverter(string comment) : base(comment){ }
}
public class JsonCommentConverter<TBase> : DefaultConverterFactory<TBase>
{
readonly string CommentWithDelimiters;
public JsonCommentConverter(string comment)
{
this.CommentWithDelimiters = " /*" + comment + "*/";
}
protected override void Write<T>(Utf8JsonWriter writer, T value, JsonSerializerOptions modifiedOptions)
{
// TODO: in .NET 9 investigate https://learn.microsoft.com/en-us/dotnet/api/microsoft.toolkit.highperformance.buffers.arraypoolbufferwriter-1 to avoid string allocations.
var json = JsonSerializer.Serialize(value, modifiedOptions);
writer.WriteRawValue(json + CommentWithDelimiters, skipInputValidation : true);
}
protected override JsonSerializerOptions ModifyOptions(JsonSerializerOptions options)
{
var modifiedOptions = base.ModifyOptions(options);
modifiedOptions.WriteIndented = false;
return modifiedOptions;
}
}
public abstract class DefaultConverterFactory<TBase> : JsonConverterFactory
{
// Adapted from this answer https://stackoverflow.com/a/78512783/3744182
// To https://stackoverflow.com/questions/78507408/in-system-text-json-is-it-possible-to-minify-only-array-items
class DefaultConverter<TConcrete> : JsonConverter<TConcrete> where TConcrete : TBase
{
readonly JsonSerializerOptions modifiedOptions;
readonly DefaultConverterFactory<TBase> factory;
public DefaultConverter(JsonSerializerOptions modifiedOptions, DefaultConverterFactory<TBase> factory) { this.modifiedOptions = modifiedOptions; this.factory = factory; }
public override void Write(Utf8JsonWriter writer, TConcrete value, JsonSerializerOptions options) { factory.Write(writer, value, modifiedOptions); }
// In .NET 9 use
// return public override TConcrete? Read
public override TConcrete Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return factory.Read<TConcrete>(ref reader, typeToConvert, modifiedOptions); }
}
protected virtual JsonSerializerOptions ModifyOptions(JsonSerializerOptions options) { return options.CopyAndRemoveConverter(this.GetType()); }
// In .NET 9 use
// return public override T? Read(
protected virtual TConcrete Read<TConcrete>(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions modifiedOptions) where TConcrete : TBase
{
// In .NET 9 use
// return (T?)
return (TConcrete)JsonSerializer.Deserialize(ref reader, typeToConvert, modifiedOptions);
}
protected virtual void Write<TConcrete>(Utf8JsonWriter writer, TConcrete value, JsonSerializerOptions modifiedOptions) where TConcrete : TBase
{
JsonSerializer.Serialize(writer, value, modifiedOptions);
}
public sealed override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var modifiedOptions = ModifyOptions(options);
if (typeToConvert == typeof(TBase))
return new DefaultConverter<TBase>(modifiedOptions, this);
else
// In .NET 9 apply ! to the end of this line to suppress a nullable warning
return (JsonConverter)Activator.CreateInstance(typeof(DefaultConverter<>).MakeGenericType(typeof(TBase), typeToConvert), new object [] {modifiedOptions, this });
}
public override bool CanConvert(Type typeToConvert) { return typeof(TBase).IsAssignableFrom(typeToConvert); }
}
public static partial class JsonExtensions
{
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;
}
}
Finally modify Person
as follows:
public class Person
{
[JsonComment("Name of the person")]
public string Name { get; set; }
[JsonComment("Age of the person")]
public int Age { get; set; }
}
And serialize it using the following JsonSerializerOptions
:
var person = new Person { Name = "Jack", Age = 22 };
var options = new JsonSerializerOptions
{
ReadCommentHandling = JsonCommentHandling.Skip,
// Add other options as required, e.g.
WriteIndented = true,
};
var json = JsonSerializer.Serialize(person, options);
var person2 = JsonSerializer.Deserialize<Person>(json, options);
And your JSON will be, as required:
{
"Name": "Jack" /*Name of the person*/,
"Age": 22 /*Age of the person*/
}
Notes:
Strictly speaking, comments are not included in the JSON standard, and System.Text.Json does not allow them by default. Nevertheless you can enable support by setting JsonCommentHandling
as shown in How can I parse JSON with comments using System.Text.Json?.
I tested my solution in .NET 9, but you are using .NET 4.8 Framework and System.Text.Json v.9.0. This is an unusual combination that does not seem to be well supported by Microsoft so you may encounter problems. Since Utf8JsonReader
is a ref struct
which was first introduced in C# 7.2, you must be sure to use this language version or later when building. To see how, check out .NET Framework 4.8 seems to use C# version older than 7.
If you do encounter problems with e.g. spans or ref structs, I would recommend sticking with Newtonsoft in .NET Framework 4.x. As you have discovered, Newtonsoft does support adding comments to property via a converter, see How do I add comments to Json.NET output? for details.
In writing my code above I tried not to use any C# language features beyond 7.3, but I did not have the tooling available to test the .NET 4.8 Framework and System.Text.Json v.9.0 combination.
Since you are targeting .NET Framework I removed all nullable annotations, but in .NET 9 one would want to apply them.
Simply calling Utf8JsonWriter.WriteCommentValue()
inside the converter like so:
JsonSerializer.Serialize(writer, value, options);
writer.WriteCommentValue(Comment);
Resulted in the comment getting placed on the next line and indented:
{
"Name": "Jack"
/*Name of the person*/,
"Age": 22
/*Age of the person*/
}
Since your question showed the comment appearing on the same line, I had to adapt the answer from In System.Text.Json is it possible to minify only array items? to disable indentation temporarily.
While this doesn't matter to you, deriving from JsonPropertyAttribute
is not supported in source generation mode. A different implementation would be required when using source generation.
Demo .NET 9 fiddle here: https://dotnetfiddle.net/Y2p2I5