Suppose I have a class with a ToString
function. I can easily customize json.net serialization so that the class is written as the string returned by ToString
. That is easy.
But I do not like the idea of creating a very short lived string just for that purpose. When serializing a lot of objects (in the hundred of thousands) this creates extra pressure on the GC.
Instead, I would like to write to the JsonWriter
directly mimicking the logic of the ToString
function, i.e. to have something like this:
class X
{
public override string ToString(){ ... }
public void Write(JsonWriter writer){ ... }
}
The json serializer will be customized to invoke X.Write
function, but the problem is that I do not know how to implement it properly so that it respects the configured formatting and all the other json settings.
My current implementation has to resort to reflection:
private static readonly Action<JsonWriter, JsonToken> s_internalWriteValue = (Action<JsonWriter, JsonToken>)Delegate
.CreateDelegate(typeof(Action<JsonWriter, JsonToken>), typeof(JsonWriter)
.GetMethod("InternalWriteValue", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic));
...
internal void Write(JsonWriter writer)
{
...
s_internalWriteValue(writer, JsonToken.String);
writer.WriteRaw("\"[");
writer.WriteRaw(statusCode);
writer.WriteRaw(",");
writer.WriteRaw(isCompilerGeneratedCode);
writer.WriteRaw(",");
writer.WriteRaw(scope);
writer.WriteRaw(",");
writer.WriteRaw(kind);
writer.WriteRaw(",");
writer.WriteRaw(rawName);
writer.WriteRaw("] ");
writer.WriteRaw(Signature);
writer.WriteRaw("\"");
}
I failed to find a solution that would use only public API. I use json.net 13.0.3
So my question is - how can we have this approach using only public json.net API?
To update the WriteState
of the incoming JsonWriter
to reflect that a value has been written, call WriteRawValue()
instead of WriteRaw()
for exactly one of your text values. This method:
Writes raw JSON where a value is expected and updates the writer's state.
Thus your Write(JsonWriter writer)
becomes:
public void Write(JsonWriter writer)
{
writer.WriteRawValue("\"["); // Write the beginning of the JSON string literal and update the WriteState
writer.WriteRaw(statusCode); // Write the remainder of the JSON string literal without changing the WriteState
writer.WriteRaw(",");
writer.WriteRaw(isCompilerGeneratedCode);
writer.WriteRaw(",");
writer.WriteRaw(scope);
writer.WriteRaw(",");
writer.WriteRaw(kind);
writer.WriteRaw(",");
writer.WriteRaw(rawName);
writer.WriteRaw("] ");
writer.WriteRaw(Signature);
writer.WriteRaw("\"");
}
Demo fiddle #1 here.
That being said, your WriteJson()
method will output malformed JSON in the event that any of your string-valued fields contain characters that must be escaped according to RFC 8259. If you want to write raw values manually you must take care of this yourself. First introduce the following extension method:
public static partial class JsonExtensions
{
static Dictionary<char, string> GetMandatoryEscapes()
{
// Standard escapes from https://www.json.org/json-en.html
var fixedEscapes = new KeyValuePair<char, string> []
{
new('\\', "\\\\"),
new('"', "\\\""), // This is correct, but System.Text.Json preferrs the longer "\\u0022" for security reasons.
new('/', "\\/"),
new('\b', "\\b"),
new('\f', "\\f"),
new('\n', "\\n"),
new('\r', "\\r"),
new('\t', "\\t"),
};
// Required control escapes from https://www.rfc-editor.org/rfc/rfc8259#section-7
var controlEscapes = Enumerable.Range(0, 0x1F + 1)
.Select(i => (char)i)
.Except(fixedEscapes.Select(p => p.Key))
.Select(c => new KeyValuePair<char, string>(c, @"\u"+((int)c).ToString("X4")));
return fixedEscapes.Concat(controlEscapes).ToDictionary(p => p.Key, p => p.Value);
}
static Dictionary<char, string> Escapes { get; } = GetMandatoryEscapes();
public static void WriteRawWithEscaping(this JsonWriter writer, string s)
{
ArgumentNullException.ThrowIfNull(writer);
if (s == null)
return;
if (s.Any(c => Escapes.ContainsKey(c)))
{
// There is no method WriteRaw(char c) so our options are to create a string for each character, or a single escaped string.
var escaped = s.Aggregate(new StringBuilder(), (sb, c) => Escapes.TryGetValue(c, out var s) ? sb.Append(s) : sb.Append(c)).ToString();
writer.WriteRaw(escaped);
}
else
writer.WriteRaw(s);
}
}
And modify your Write(JsonWriter writer)
method as follows:
public void Write(JsonWriter writer)
{
//s_internalWriteValue(writer, JsonToken.String);
writer.WriteRawValue("\"[");
writer.WriteRawWithEscaping(statusCode);
writer.WriteRaw(",");
writer.WriteRawWithEscaping(isCompilerGeneratedCode);
writer.WriteRaw(",");
writer.WriteRawWithEscaping(scope);
writer.WriteRaw(",");
writer.WriteRawWithEscaping(kind);
writer.WriteRaw(",");
writer.WriteRawWithEscaping(rawName);
writer.WriteRaw("] ");
writer.WriteRawWithEscaping(Signature);
writer.WriteRaw("\"");
}
Your JSON should now be well formed. Demo fiddle #2 here.