8

Deserializing using JsonSerialize.DeserializeAsync and a custom converter, e.g.

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

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

Here I would get all string properties, which is kind of okay, though is there any way to check the property name for a given value, e.g. something like this, where to process only the Body property:

class MyMailContent 
{
    public string Name { get; set; }
    public string Subject { get; set; }
    public string Body { get; set; }
}

public class MyStringJsonConverter : JsonConverter<string>
{
    public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.PropertyName.Equals("Body"))
        {
            var s = reader.GetString();
            //do some process with the string value
            return s;
        }

        return reader.GetString();
    }
}

Or is there some other way to single out a given property?

Note, I am looking for a solution using System.Text.Json.

Asons
  • 84,923
  • 12
  • 110
  • 165
  • 1
    Are you implying without saying it that you don't want to decorate the `Body` property with `[JsonConverter(typeof(MyStringJsonConverter))]`? – Jimi May 23 '21 at 13:44
  • @Jimi -- No I wasn't. I still would like to have a solution not depending on decorating properties all over the place, but I haven't found how decorating a property would make the converter kick in only on a decorated one. Please provide an answer for that. – Asons May 23 '21 at 13:50
  • If you are looking for the name of the parent property when reading the property value, or more generally the ***path*** to the current value, then it seems that `System.Text.Json` does not make that available. It tracks it internally -- it's in [`ReadStack.JsonPath()`](https://github.com/dotnet/runtime/blob/798949a6f8013fc457cecca1f291a930670cd913/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs#L234) but `ReadStack` is internal and never passed to applications code. – dbc May 23 '21 at 14:17
  • Also, Jimi asked, *Are you implying that you don't want to decorate the Body property with `[JsonConverter(typeof(MyStringJsonConverter))]`* and you answered, *No I wasn't.* So, given that you are willing to decorate `Body` with some converter attribute, can't you use a specific converter for that property? If not, might you share a [mcve]? – dbc May 23 '21 at 14:21
  • @dbc IIRC, you can deserialize the whole thing *manually*, in a `while(reader.Read())` loop, check whether `reader.TokenType == JsonTokenType.PropertyName`, then read the next value. I'm not sure it's worth it here, since - as it appears - the custom deserialization always applies to the same property, so decorating that property seems a better choice. – Jimi May 23 '21 at 14:29
  • @dbc -- Thanks for commenting. Your first comment answer my main question, so posting that as an answer would probably be good for future users. Your second comment, I did answer Jimi saying _using decoration could be one solution, I just haven't found how-to for it to only fire on a single property_, and asked them to post such solution. – Asons May 23 '21 at 14:31
  • @dbc When `.TokenType = JsonTokenType.PropertyName`, then `reader.GetString();` should return the property name (cannot test right now, it's an IIRC). – Jimi May 23 '21 at 14:34
  • @Jimi - that's true, but in a `JsonConverter` the `TokenType` will generally not be `JsonTokenType.PropertyName`, unless OP is writing a converter to deserialize a complex object as a single string. I *think* what OP wants to do here is to create a converter that preprocesses every string -- e.g. by trimming it -- but has different logic for certain string properties. But I'm not sure and so a [mcve] would clarify things. – dbc May 23 '21 at 14:37
  • @dbc I actually meant a `JsonConverter` with *deserialize the whole thing manually*, but (always IIRC) `JsonConverter` should also include `TokenType.PropetyName`. -- I'll test it when I can (but, you know this stuff better than I do...). – Jimi May 23 '21 at 14:41
  • @dbc Right, I mean, always handling the `reader` directly (given the *nature* of the class shown here). – Jimi May 23 '21 at 14:56
  • @Ason - *I just haven't found how-to for it to only fire on a single property* -- you know you can just apply [`[System.Text.Json.Serialization.JsonConverter(typeof(MyBodyStringJsonConverter))]`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonconverter-1?view=net-5.0) to `public string Body { get; set; }`, and it will only fire for the `Body` string property, right? – dbc May 23 '21 at 15:21
  • @dbc -- I didn't know, and while searching all over didn't find one could do that for a single property. – Asons May 23 '21 at 15:44
  • @Jimi -- I tested using `reader.read()`, and with that one can actually step through all properties/values, exactly what I asked for, so please do provide an answer with a sample doing that, and I will spend a bounty on it. Still, what "dbc" posted, decorate a single property might be more performant/simpler in some cases, though in my solution we do a lot of caching, so even if parsing the whole property list is less performant, we most of the time use a cached object where it matters much less. – Asons May 23 '21 at 19:01
  • Well, since you seem to have it already sorted out, you can post it yourself. I'll delete the comments. -- If you mean to implement what's called *polymorphic deserialization* (System.Text.Json is still missing some features that are commonly used in Json.Net), thus handling `[Utf8JsonReader].Read()` directly, possibly with a custom `JsonConverter`. -- I'll set a watch on this question, to add an alternative solution, eventually. – Jimi May 24 '21 at 09:22
  • @Jimi -- So I did, and thanks for the help. – Asons May 24 '21 at 12:06

2 Answers2

4

Thanks to Jimi for the comment, and with his approval, I post a wiki answer using his suggested solution, and anyone who has more to contribute, please feel free to do so.

In most cases the accepted answer is a good one, though if one still need to process one or more specific properties by their property name, here is one way to do that.

public class MyMailContent
{
    public string Name { get; set; }
    public string Subject { get; set; }
    public string Body { get; set; }
}

public class MyMailContentJsonConverter : JsonConverter<MyMailContent>
{
    public override MyMailContent Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
        {
            throw new JsonException();
        }

        var mailContent = new MyMailContent();

        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
            {
                return mailContent;
            }

            if (reader.TokenType == JsonTokenType.PropertyName)
            {
                var propertyName = reader.GetString();

                reader.Read();

                switch (propertyName)
                {
                    case "Name":
                        mailContent.Name = reader.GetString();
                        break;

                    case "Subject":
                        mailContent.Subject = reader.GetString();
                        break;

                    case "Body":
                        string body = reader.GetString();
                        //do some process on body here
                        mailContent.Body = body;
                        break;
                }
            }
        }

        throw new JsonException();
    }

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

private static JsonSerializerOptions _jsonDeserializeOptions = new()
{
    ReadCommentHandling = JsonCommentHandling.Skip,
    AllowTrailingCommas = true,
    Converters =
    {
        new MyMailContentJsonConverter()
    }
};

And use it like this

var jsonstring = JsonSerializer.Serialize(new MyMailContent
{
    Name = "some name",
    Subject = "some subject",
    Body = "some body"
});

var MailContent = JsonSerializer.Deserialize<MyMailContent>(jsonstring, _jsonDeserializeOptions);
Asons
  • 84,923
  • 12
  • 110
  • 165
3

System.Text.Json does not make the parent property name, or more generally the path to the current value, available inside JsonConverter<T>.Read(). This information is tracked internally -- it's in ReadStack.JsonPath() -- but ReadStack is internal and never passed to applications code.

However, as explained in Registration sample - [JsonConverter] on a property, you can apply your MyStringJsonConverter directly to public string Body { get; set; } by using JsonConverterAttribute:

class MyMailContent 
{
    public string Name { get; set; }
    public string Subject { get; set; }
    [JsonConverter(typeof(MyStringJsonConverter))]
    public string Body { get; set; }
}

By doing this, MyStringJsonConverter.Read() and .Write() will fire only for MyMailContent.Body. Even if you have some overall JsonConverter<string> in JsonSerializerOptions.Converters, the converter applied to the property will take precedence:

During serialization or deserialization, a converter is chosen for each JSON element in the following order, listed from highest priority to lowest:

  • [JsonConverter] applied to a property.
  • A converter added to the Converters collection.
  • [JsonConverter] applied to a custom value type or POCO.

(Note this differs partially from Newtonsoft where converters applied to types supersede converters in settings.)

dbc
  • 104,963
  • 20
  • 228
  • 340