System.Text.Json - Deserialize nested object as string

Solution 1:

Found a right way how to correctly read the nested JSON object inside the JsonConverter. The complete solution is the following:

public class SomeModel
{
    public int Id { get; set; }

    public string Name { get; set; }

    [JsonConverter(typeof(InfoToStringConverter))]
    public string Info { get; set; }
}

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

    public override void Write(
        Utf8JsonWriter writer, string value, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }
}

In the code itself there is no need even to create options:

var json = @"{
                 ""Id"": 1,
                 ""Name"": ""Some Name"",
                 ""Info"": {
                     ""Additional"": ""Fields"",
                     ""Are"": ""Inside""
                 }
             }";

var model = JsonSerializer.Deserialize<SomeModel>(json);

The raw JSON text in the Info property contains even extra spaces introduced in the example for nice readability.

And there is no mixing of model representation and its serialization as remarked @PavelAnikhouski in his answer.

Solution 2:

You can use a JsonExtensionData attribute for that and declare a Dictionary<string, JsonElement> or Dictionary<string, object> property in your model to store this information

public class SomeModel
{
    public int Id { get; set; }
    public string Name { get; set; }

    [JsonExtensionData]
    public Dictionary<string, JsonElement> ExtensionData { get; set; }

    [JsonIgnore]
    public string Data
    {
        get
        {
            return ExtensionData?["Info"].GetRawText();
        }
    }
}

Then you can add an additional property to get a string from this dictionary by Info key. In code above the Data property will contain the expected string

{
    "Additional": "Fields",
    "Are": "Inside"
}

For some reasons adding the property with the same name Info doesn't work, even with JsonIgnore. Have a look at Handle overflow JSON for details.

You can also declare the Info property as JsonElement type and get raw text from it

public class SomeModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public JsonElement Info { get; set; }
}
var model = JsonSerializer.Deserialize<SomeModel>(json);
var rawString = model.Info.GetRawText();

But it will cause a mixing of model representation and its serialization.

Another option is to parse the data using JsonDocument, enumerate properties and parse them one by one, like that

var document = JsonDocument.Parse(json);
foreach (var token in document.RootElement.EnumerateObject())
{
    if (token.Value.ValueKind == JsonValueKind.Number)
    {
        if(token.Value.TryGetInt32(out int number))
        {
        }
    }
    if (token.Value.ValueKind == JsonValueKind.String)
    {
        var stringValue = token.Value.GetString();
    }
    if (token.Value.ValueKind == JsonValueKind.Object)
    {
        var rawContent = token.Value.GetRawText();
    }
}

Solution 3:

a quick addendum to the accepted answer:

If you need to write raw JSON values as well, here is an implementation of the Write method for the converter:

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

As outlined in the dotnet runtime repo on github, this seems to be the "proper" way to workaround the fact that they decided not to implement a WriteRawValue method.