System.Text.Json - Deserialize nested object as string
Asked Answered
L

4

33

I'm trying to use the System.Text.Json.JsonSerializer to deserialize the model partially, so one of the properties is read as string that contains the original JSON.

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

The example code

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

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

should produce the model, which Info property contains the Info object from the original JSON as string:

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

It doesn't work out of the box and throws an exception:

System.Text.Json.JsonException: ---> System.InvalidOperationException: Cannot get the value of a token type 'StartObject' as a string.

What have I tried so far:

public class InfoToStringConverter : JsonConverter<string>
{
    public override string Read(
        ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
    {
        return reader.GetString();
    }

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

and apply it in the model as

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

and add in the options to JsonSerializer

var options = new JsonSerializerOptions();
options.Converters.Add(new InfoToStringConverter());
var model = JsonSerializer.Deserialize<SomeModel>(json, options);

Still, it throws the same exception:

System.Text.Json.JsonException: ---> System.InvalidOperationException: Cannot get the value of a token type 'StartObject' as a string.

What is the right recipe to cook what I need? It worked in a similar way using Newtonsoft.Json.

Update

For me it is important to keep the nested JSON object as original as possible. So, I'd avoid options like to deserialize as Dictionary and serialize back, because I'm afraid to introduce undesirable changes.

Latonialatoniah answered 25/2, 2020 at 18:55 Comment(2)
You can also create a class and store the Info value in its instance, like it's done in this threadKortneykoruna
To achieve the expected behavior with custom converter you can have a look at JsonValueConverterKeyValuePair and see how to properly read and write StartObject and EndObject of complicated json structureKortneykoruna
L
49

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.

Latonialatoniah answered 25/2, 2020 at 20:23 Comment(3)
Good job, you've figured out how to do it with custom converter:) Just a quick note, without Write implementation you can't serialize it backKortneykoruna
This is great thank you. You might make a small performance improvement if the value is null : if(reader.TokenType == JsonTokenType.Null) { return null; }Height
for me it doesn't work if the type of Info is a record that contains a single string field, because jsonDoc.RootElement.GetRawText() returns the double quotes around the string value as part of the string! If I just try to brute-force it (remove the quotes myself) I trap myself into a corner as later on another deserialization inexplicably provides Info (jsonDoc.rootElement) already as an Object instead of a String, so the brute force method fails.Beggarly
K
9

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();
    }
}
Kortneykoruna answered 25/2, 2020 at 19:6 Comment(0)
M
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.

Monkhood answered 15/7, 2021 at 13:28 Comment(0)
R
0

I create a text converter as solution to the problem. I use part of the solution given by kyrylc, and create a custom text converter that manage all that I need with string, in this case I don't need to use a header:

Converter:

public class StringDoubleConverter : System.Text.Json.Serialization.JsonConverter<string>
{
    /// <summary>
    /// Allow to convert a Double number to string
    /// </summary>
    /// <param name="reader"></param>
    /// <param name="typeToConvert"></param>
    /// <param name="options"></param>
    /// <returns></returns>
    public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {

        if (reader.TokenType == JsonTokenType.Number)
        {
            var stringValue = reader.GetDouble();
            return stringValue.ToString();
        }
        else if (reader.TokenType == JsonTokenType.String)
        {
            return reader.GetString();
        }

        using var jsonDoc = JsonDocument.ParseValue(ref reader);
        return jsonDoc.RootElement.GetRawText();

    }

    /// <summary>
    /// Write the value
    /// </summary>
    /// <param name="writer"></param>
    /// <param name="value"></param>
    /// <param name="options"></param>
    public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value);
    }

}

I added the converter to JsonSerializerOptions (please ignore the setting that you don't need)

      this.serializerOptionReceive = new JsonSerializerOptions()
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            
            NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString,
            DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
            WriteIndented = true
        };
       serializerOptionReceive.Converters.Add(new StringDoubleConverter());

and use the created serialization option, when I deserialize the object..

JsonSerializer.Deserialize<yourobject>(contentResponse, serializerOptionReceive);
Raynell answered 4/4, 2023 at 18:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.