1

I know from the docs that this is technically not possible (Utf8JsonReader is forward-only with no caching mechanism), but I want to know if I can achieve something equivalent in behaviour.

Below is my use case and the problem I have with Utf8JsonReader's incapability of resetting to its start position.


I have some classes and subclasses that I want to (de-)serialize to JSON using System.Text.Json. Serialization works perfectly without any custom JsonConverter needed; I just need to add a JsonStringEnumConverter.

public class Foo
{
    public Foo(BarBase bar) { Bar = bar; }

    public BarBase Bar { get; init; }
}

public class BarBase
{
    public BarBase(string text, BarType type) { Text = text; Type = type; }

    public string Text { get; init; }
    public BarType Type { get; init; }
}

public class BarSub1 : BarBase
{
    public BarSub1(string text) : base(text, BarType.sub1) {}
}

public class BarSub2 : BarBase
{
    public BarSub2(string text) : base(text, BarType.sub2) {}
}

public enum BarType { base, sub1, sub2 }

Here is the serialization:

var foo = new Foo(new BarSub1("bar"));
var json = JsonSerializer.Serialize(foo,
    new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } };
// json is { "Bar": { "Text": "bar", "Type": "sub1" } }

Deserialization does not work out of the box (it does not throw errors but it always deserializes my Bar objects to BarBase, which I don't want); for that I need a JsonConverter according to the answers here and here. So I tried writing one, but can't get it working. The problem is: In order to determine the type I want to deserialize, I need to read a bit of the Json (until I get the Type property). So the Utf8JsonReader is not at its starting position any more. But since the Type property is part of the class (and part of what the JsonSerializer handles), I cannot start deserializting with the JsonSerializer.Deserialize method after I have already ran over the Type property. I would need to call JsonSerializer.Deserialize before any Read()ing was done.

public class BarConverter : JsonConverter<BarBase>
{
    public override BarBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        Type BarTypeToType(BarType t)
        {
            switch (t)
            {
                case base: return typeof(BarBase);
                case sub1: return typeof(BarSub1);
                case sub2: return typeof(BarSub2);
                default: throw new ArgumentException();
            }
        }
    
        if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException("missing start object");

        bool discriminatorFound = false;
        while (reader.Read())
        {
            if (reader.TokenType != JsonTokenType.PropertyName)
                continue;
            if (reader.GetString() != "Type")
                continue;
            discriminatorFound = true;
            break;
        }

        if (!discriminatorFound)
            throw new JsonException($"type discriminator property not found");

        if (!reader.Read() || reader.TokenType != JsonTokenType.String)
            throw new JsonException("type discriminator value does not exist or is not a string");

        var typeString = reader.GetString();
        if (!Enum.TryParse(typeString, out BarType enumValue))
            throw new JsonException($"value {typeString} is not an element of {nameof(BarType)}");

        // PROBLEM: This doesn't work, since we are not at the start position any more!
        BarBase instance = (BarBase)JsonSerializer.Deserialize(ref reader, BarTypeToType(enumValue), options)!;

        if (!reader.Read() || reader.TokenType != JsonTokenType.EndObject)
            throw new JsonException("missing end object");

        return instance;
    }

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

Can someone help?

Kjara
  • 2,504
  • 15
  • 42
  • 1
    Take a look at [this answer specifically](https://stackoverflow.com/a/66352365/3744182) by [Somaar](https://stackoverflow.com/users/2342856/somaar) to [Is polymorphic deserialization possible in System.Text.Json?](https://stackoverflow.com/q/58074304/3744182). It takes advantage of the fact that `Utf8JsonWriter` is a **`struct`** to make a backup copy, then re-read starting from the backup copy. Do note ***this only works within the context of `JsonConverter.Read()`*** because the entire JSON stream is preloaded before the call; outside of `Read()` you need to deal with asynchrony. – dbc Mar 08 '22 at 15:32
  • The fact that copying the `Utf8JsonReader` works is confirmed by [this comment](https://stackoverflow.com/questions/58074304/is-polymorphic-deserialization-possible-in-system-text-json/66352365#comment115472745_59744873) by [ahsonkhan](https://stackoverflow.com/users/12509023/ahsonkhan) who was I believe one of the original MSFT developers. But it only works because `JsonSerializer` preloads the entire JSON sequence before calling the converter, if MSFT ever changes that design copying the reader might stop working (while loading into `JsonDocument` is guaranteed to work). – dbc Mar 08 '22 at 17:08
  • Do you still need an answer to this? – dbc Mar 14 '22 at 21:36
  • @dbc No, you already gave an answer in your first comment. – Kjara Mar 16 '22 at 08:26
  • 1
    @dbc Utf8JsonWriter is definitely not a struct like the reader. I assume this was just a typo given the question, your comment, and your linked answer are all about deserialization, but I thought it best to clarify because Google is bringing in people (me) looking specifically for information about Utf8JsonWriter. – Daniel Mar 13 '23 at 02:31
  • @Daniel - Thanks, you're right, my comment should have said *It takes advantage of the fact that **`Utf8JsonReader`** is a `struct` to make a backup copy*. But of course I can't edit my comment any longer, which is just another reason that I/we shouldn't be answering in comments. – dbc Mar 13 '23 at 03:57

0 Answers0