A System.Text.Json converter to serialize/deserialize a JSON node as a string

How to deserialize a part of JSON as a string keeping everything inside as is including the formatting? Or how to merge JSONs together into one having them provided as strings? The article shows an implement of a custom converter for the System.Text.Json that does described things, and provides additional cases when such converter might be useful.

Imagine a case when a service is responsible only for transfering data into subsystems. A typical request for such a service might be in JSON format and consist of two parts: the actual payload to transfer and additional metadata:

{
  "Source": "some-system-id",
  "Target": "another-system-id",
  "CreatedAt": "2020-01-11T12:00:00Z",
  "Payload": {
    "some": "payload",
    "with": {
        "fields": "which",
        "must": "be",
        "transfered": "exactly"
    },
    "as": "is"
  }
}

Logically, the service shouldn't change the payload and ideally transfer the payload as is including the original formatting. One of the possible solutions could be to define the model as following:

public record Request
{
    public string Source { get; init; }
    public string Target { get; init; }
    public DateTime CreatedAt { get; init; }
    public object Payload { get; init; }
}

In this case the Payload object will be deserialized first into dynamic structure, and then must be serialized back before sending further. The serialization will rewrite the formatting and potentially might corrupt date and time values.

The more general approach is to keep the Payload as a string:

public record Request
{
    public string Source { get; init; }
    public string Target { get; init; }
    public DateTime CreatedAt { get; init; }
    public string Payload { get; init; }
}

In this case the service doesn't care what is inside of Payload JSON element, and doesn't have to serialize it back before sending. But to make it possible a custom JCON converter is needed:

public class StringToRawTextJsonConverter : JsonConverter<string>
{
    public override string Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options)
    {
        using var jsonDoc = JsonDocument.ParseValue(ref reader);

        var element = jsonDoc.RootElement;
        return element.ValueKind == JsonValueKind.String
            ? element.GetString()
            : element.GetRawText();
    }

    public override void Write(
        Utf8JsonWriter writer,
        string value,
        JsonSerializerOptions options)
    {
        using var jsonDoc = JsonDocument.Parse(value);
        jsonDoc.RootElement.WriteTo(writer);
    }
}

The first thing converter does during the deserialization (Read() method) is parsing JSON element as a JsonDocument. At this point it is possible to check:

  • if the JSON root element is a simple string then nothing is required, just take its value,
  • otherwise, read the whole element as a raw text.

During the serialization (Write() method) the converter does the opposite thing: it writes the raw text from the string property directly.

The converter must be attached directly on the Payload property:

public record Request
{
    // ...

    [JsonConverter(typeof(StringToRawTextJsonConverter))]
    public string Payload { get; init; }
}

As a result, after serialization, the Payload property will contain the string representation of the original JSON node, including even indentation.

Basically, such approach might be useful in cases when a part of the JSON must be provided untouched (e.g. transfered further, saved to DB or logged), or is given as a string, and must be rendered as a part of a final JSON.

previous post: The new blog